Injection de dépendances dans Android

L'injection de dépendances est une technique couramment utilisée en programmation et parfaitement adaptée au développement sur Android. En suivant les principes d'injection de dépendances, vous pourrez jeter les bases d'une architecture d'application de qualité.

L'implémentation de cette technique vous garantit les avantages suivants :

  • Code réutilisable
  • Facilité de refactorisation
  • Facilité de test

Principes de base de l'injection de dépendances

Avant d'aborder plus précisément l'injection de dépendances sur Android, cette page présente le fonctionnement général de l'injection de dépendances.

Qu'est-ce que l'injection de dépendances ?

Les classes nécessitent souvent des références à d'autres classes. Par exemple, une classe Car peut nécessiter une référence à une classe Engine. Ces classes requises sont appelées dépendances et, dans cet exemple, la classe Car dépend de l'exécution d'une instance de la classe Engine.

Une classe peut obtenir un objet de trois manières :

  1. La classe construit la dépendance dont elle a besoin. Dans l'exemple ci-dessus, Car crée et initialise sa propre instance de Engine.
  2. Elle le récupère à partir d'autre chose. Certaines API Android, telles que les getters Context et getSystemService(), fonctionnent de cette manière.
  3. Elle l'obtient sous forme de paramètre. L'application peut fournir ces dépendances lorsque la classe est construite ou les transmettre aux fonctions qui nécessitent chaque dépendance. Dans l'exemple ci-dessus, le constructeur Car recevra Engine comme paramètre.

La troisième option est l'injection de dépendances. Avec cette approche, vous prenez les dépendances d'une classe et vous les fournissez. Les classes ne les récupèrent donc pas elles-mêmes.

Exemple : Sans injection de dépendances, voici à quoi ressemble une Car qui crée sa propre dépendance Engine dans le code :

Kotlin

class Car {

    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

Java

class Car {

    private Engine engine = new Engine();

    public void start() {
        engine.start();
    }
}


class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}
Classe Car sans injection de dépendances

Il ne s'agit pas d'un exemple d'injection de dépendances, car la classe Car construit sa propre dépendance Engine. Cela peut être problématique pour les raisons suivantes :

  • Car et Engine sont étroitement liés : une instance de Car utilise un seul type d'Engine, et aucune sous-classe ou autre implémentation ne peut être utilisée facilement. Si Car devait construire sa propre dépendance Engine, vous devriez créer deux types de Car au lieu de simplement réutiliser le même Car pour les moteurs de type Gas et Electric.

  • La dépendance physique à Engine rend les tests plus difficiles. Car utilise une instance réelle de Engine, ce qui vous empêche d'utiliser un double de test pour modifier Engine pour différents scénarios de test.

À quoi ressemble le code avec l'injection de dépendances ? Au lieu que chaque instance de Car crée son propre objet Engine lors de l'initialisation, elle reçoit un objet Engine en tant que paramètre dans son constructeur :

Kotlin

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

Java

class Car {

    private final Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}


class MyApp {
    public static void main(String[] args) {
        Engine engine = new Engine();
        Car car = new Car(engine);
        car.start();
    }
}
Classe Car utilisant l'injection de dépendances

La fonction main utilise Car. Comme Car dépend de Engine, l'application crée une instance de Engine, puis l'utilise pour construire une instance de Car. Les avantages de cette approche basée sur l'injection de dépendances sont les suivants :

  • Réutilisabilité de Car. Vous pouvez transmettre différentes implémentations d'Engine à Car. Par exemple, vous pouvez définir une nouvelle sous-classe d'Engine appelée ElectricEngine qui sera utilisée par Car. Si vous utilisez l'injection de dépendances, il vous suffit de transmettre une instance de la sous-classe ElectricEngine mise à jour. Car fonctionnera toujours sans aucune autre modification.

  • Test simplifié de Car. Vous pouvez utiliser des doubles de test pour tester vos différents scénarios. Par exemple, vous pouvez créer un double d'Engine nommé FakeEngine et le configurer pour différents tests.

Il existe deux façons principales d'injecter des dépendances dans Android :

  • Injection par constructeur. C'est ce qui est décrit ci-dessus. Vous transmettez les dépendances d'une classe à son constructeur.

  • Injection par champs (ou injection par Setter). Certaines classes du framework Android, telles que les activités et les fragments, sont instanciées par le système. Les injections de constructeur ne sont donc pas possibles. Avec l'injection de champs, les dépendances sont instanciées après la création de la classe. Le code se présente comme suit :

Kotlin

class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

Java

class Car {

    private Engine engine;

    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.setEngine(new Engine());
        car.start();
    }
}

Injection de dépendances automatisée

Dans l'exemple précédent, vous avez créé, fourni et géré vous-même les dépendances des différentes classes, sans utiliser de bibliothèque. C'est ce qu'on appelle l'injection manuelle de dépendances. Dans l'exemple de Car, il n'y a qu'une seule dépendance, mais si les dépendances et les classes sont nombreuses, ce processus peut être fastidieux. L'injection de dépendances manuelle présente également plusieurs problèmes :

  • Pour les applications volumineuses, prendre toutes les dépendances et les connecter correctement peut nécessiter une grande quantité de code récurrent. Dans une architecture multicouche, vous devez fournir toutes les dépendances des couches inférieures pour pouvoir créer un objet pour une couche supérieure. Prenons un exemple concret : la construction d'une vraie voiture aurait besoin d'un moteur, d'une transmission, d'un châssis et d'autres pièces. Un moteur, à son tour, a besoin de cylindres et de bougies d'allumage.

  • Si vous ne pouvez pas construire des dépendances avant de les transmettre (par exemple si vous utilisez des initialisations différées, ou si vous définissez des flux de votre application comme champ d'application de certains objets), vous devez écrire et gérer un conteneur personnalisé (ou graphique de dépendances) qui gère les durées de vie de vos dépendances en mémoire.

Des bibliothèques permettent de résoudre ce problème en automatisant le processus de création et de fourniture des dépendances. Elles se répartissent en deux catégories :

  • Solutions basées sur la réflexivité, qui connectent les dépendances au moment de l'exécution

  • Solutions statiques, qui génèrent le code permettant de connecter les dépendances au moment de la compilation

Dagger est une bibliothèque d'injection de dépendances populaire pour Java, Kotlin et Android. Elle est gérée par Google. Dagger facilite l'utilisation de l'injection de dépendances dans votre application en créant et en gérant le graphique des dépendances pour vous. Elle fournit des dépendances entièrement statiques et au moment de la compilation afin de résoudre de nombreux problèmes de développement et de performances liées aux solutions basées sur la réflexivité, telles que Guice.

Alternatives à l'injection de dépendances

Une alternative à l'injection de dépendances consiste à utiliser un localisateur de services. Le modèle de conception du localisateur de services améliore également le découplage des classes à partir des dépendances concrètes. Vous allez créer une classe ServiceLocator, qui crée et stocke les dépendances, puis les fournit à la demande.

Kotlin

object ServiceLocator {
    fun getEngine(): Engine = Engine()
}

class Car {
    private val engine = ServiceLocator.getEngine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

Java

class ServiceLocator {

    private static ServiceLocator instance = null;

    private ServiceLocator() {}

    public static ServiceLocator getInstance() {
        if (instance == null) {
            synchronized(ServiceLocator.class) {
                instance = new ServiceLocator();
            }
        }
        return instance;
    }

    public Engine getEngine() {
        return new Engine();
    }
}

class Car {

    private Engine engine = ServiceLocator.getInstance().getEngine();

    public void start() {
        engine.start();
    }
}

class MyApp {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}

Le modèle du localisateur de services est différent de l'injection de dépendances dans la manière dont les éléments sont utilisés. Avec ce modèle, les classes ont le contrôle et demandent l'injection d'objets. Avec l'injection de dépendances, c'est l'application qui a le contrôle et injecte les objets requis de manière proactive.

Comparaison avec l'injection de dépendances :

  • La collection de dépendances requise par un localisateur de services complique les tests, car ils doivent tous interagir avec le même localisateur de services global.

  • Les dépendances sont encodées dans l'implémentation de la classe, et non dans la surface de l'API. Par conséquent, il est plus difficile de déterminer ce dont une classe a besoin de l'extérieur. Les modifications apportées à Car ou aux dépendances disponibles dans le localisateur de services peuvent donc entraîner un échec des références, et l'exécution ou les tests risquent de ne pas aboutir.

  • Il est plus difficile de gérer la durée de vie des objets si vous souhaitez que leur champ d'application dépasse la durée de vie totale de l'application.

Utiliser Hilt dans votre application Android

Hilt est la bibliothèque de Jetpack recommandée pour l'injection de dépendances dans Android. Elle définit une méthode standard d'injection de dépendances dans votre application en fournissant des conteneurs pour chaque classe Android de votre projet, et en gérant automatiquement leur cycle de vie.

Hilt repose sur la bibliothèque d'injection de dépendances Dagger et bénéficie ainsi de l'exactitude du temps de compilation, des performances d'exécution, de l'évolutivité et de la compatibilité avec Android Studio qu'offre Dagger.

Pour en savoir plus sur Hilt, consultez la page Injection de dépendances avec Hilt.

Conclusion

L'injection de dépendances offre à votre application les avantages suivants :

  • Réutilisabilité des classes et découplage des dépendances : il est plus facile d'échanger les implémentations d'une dépendance. La réutilisation du code est améliorée grâce à l'inversion du contrôle. Les classes ne contrôlent plus la façon dont leurs dépendances sont créées, mais sont compatibles avec toutes les configurations.

  • Facilité de refactorisation : les dépendances deviennent une partie vérifiable de la surface de l'API. Elles peuvent donc être vérifiées au moment de la création des objets ou de la compilation, plutôt que d'être masquées en tant que détails d'implémentation.

  • Facilité de test : une classe ne gère pas ses dépendances. Lorsque vous la testez, vous pouvez donc choisir différentes implémentations pour tester tous vos scénarios.

Pour bien comprendre les avantages de l'injection de dépendances, essayez de l'exécuter manuellement dans votre application, comme indiqué dans la section Injection manuelle de dépendances.

Ressources supplémentaires

Pour en savoir plus sur l'injection de dépendances, consultez les ressources supplémentaires suivantes.

Exemples