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 :
- La classe construit la dépendance dont elle a besoin. Dans l'exemple ci-dessus,
Car
crée et initialise sa propre instance deEngine
. - Elle la récupère à partir d'autre chose. Certaines API Android, telles que les getters
Context
etgetSystemService()
, fonctionnent de cette manière. - 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
recevraEngine
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(); } }

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
etEngine
sont étroitement liés : une instance deCar
utilise un type d'Engine
, et aucune sous-classe ou autre mise en œuvre ne peut être facilement utilisée. SiCar
devait construire son propreEngine
, vous devriez créer deux types deCar
au lieu de simplement réutiliser le mêmeCar
pour les moteurs de typeGas
etElectric
.La forte dépendance à
Engine
rend les tests plus difficiles.Car
utilise une instance réelle deEngine
, ce qui vous empêche d'utiliser un double de test pour modifierEngine
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(); } }

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éeElectricEngine
que vous souhaitez queCar
utilise. Si vous utilisez l'injection de dépendances, il vous suffit de transmettre une instance de la sous-classeElectricEngine
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.