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 la récupère à partir d'autre chose. Certaines API Android, telles que les getters Context et getSystemService(), fonctionnent de cette manière.
  3. Elles sont indiquées en tant que 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 Engine. Cela peut être problématique pour les raisons suivantes :

  • Car et Engine sont étroitement liés : une instance de Car utilise un type d'Engine, et aucune sous-classe ou autre mise en œuvre ne peut être facilement utilisée. Si Car devait construire son propre 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 forte dépendance à 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éutilisation 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 que vous souhaitez que Car utilise. 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 passer 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 de constructeur. C'est ce qui est décrit ci-dessus. Vous transmettez les dépendances d'une classe à son constructeur.

  • Injection de champs (ou injection de 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.

  • Lorsque vous n'êtes pas en mesure de construire des dépendances avant de les transmettre (par exemple, lorsque vous utilisez des initialisations différées ou des objets de champ d'application vers des flux de votre application), 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éflexion qui connectent les dépendances au moment de l'exécution

  • Des 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 des solutions basées sur la réflexion telles que Guice.

Alternatives à l'injection de dépendances

Une alternative à l'injection de dépendances consiste à utiliser un outil de localisation de services. Le modèle de conception de l'outil de localisation de services améliore également le découplage des classes des dépendances concrètes. Vous allez créer une classe appelée outil de localisation de services, 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 de l'outil de localisation de services est différent de l'injection de dépendances dans la manière dont les éléments sont utilisés. Avec le modèle de localisation de services, les classes ont le contrôle et demandent l'injection d'objets. Avec l'injection de dépendances, l'application contrôle et injecte de manière proactive les objets requis.

Comparaison avec l'injection de dépendances :

  • La collection de dépendances requise par un outil de localisation de services complique la tâche de test, car tous les tests doivent interagir avec le même outil de localisation 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. Par conséquent, les modifications apportées à Car ou aux dépendances disponibles dans l'outil de localisation de services peuvent entraîner des échecs d'exécution ou de test, ce qui entraîne l'échec des références.

  • Il est plus difficile de gérer les durées de vie des objets si vous souhaitez que leur portée dépasse le cadre de l'application entière.

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épendance 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éutilisation 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, et 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 l'ensemble de vos différents cas.

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