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 le récupère à partir d'autre chose. Certaines API Android, telles que les getters
Context
etgetSystemService()
, fonctionnent de cette manière. - 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
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(); } }
![Classe Car sans injection de dépendances](https://developer.android.com/static/images/training/dependency-injection/1-car-engine-no-di.png?authuser=6&hl=fr)
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
etEngine
sont étroitement liés : une instance deCar
utilise un seul type d'Engine
, et aucune sous-classe ou autre implémentation ne peut être utilisée facilement. SiCar
devait construire sa propre dépendanceEngine
, vous devriez créer deux types deCar
au lieu de simplement réutiliser le mêmeCar
pour les moteurs de typeGas
etElectric
.La dépendance physique à
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(); } }
![Classe Car utilisant l'injection de dépendances](https://developer.android.com/static/images/training/dependency-injection/1-car-engine-di.png?authuser=6&hl=fr)
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éeElectricEngine
qui sera utilisée parCar
. 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 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.