Introduction à l'Injection de Dépendances en Kotlin
Le développement moderne en Kotlin nécessite une gestion compétente des dépendances entre les classes. Dependency Injection (DI) est un patron de conception qui permet de transmettre les dépendances d'un objet depuis l'extérieur, plutôt que de les créer à l'intérieur. Cela réduit le couplage du code, simplifie les tests et rend l'application plus flexible.
Dans cet article, nous allons voir ce qu'est l'injection de dépendances, pourquoi elle est nécessaire, et examiner trois approches pour sa mise en œuvre en Kotlin : l'injection manuelle, l'utilisation de la bibliothèque Hilt et du framework léger Koin. Ce contenu sera utile aussi bien aux débutants qu'aux développeurs expérimentés souhaitant structurer l'architecture de leurs projets.
Fondamentaux de l'Injection de Dépendances
Le problème sans DI
Imaginez une classe UserService qui crée directement une instance de UserRepository :
class UserService { private val repository = UserRepository() // couplage fort
fun getUser(id: Int): User { return repository.findById(id) }}Un tel code est difficile à tester : vous ne pouvez pas remplacer UserRepository par un objet mock. De plus, si le constructeur de UserRepository change, vous devrez modifier tous les endroits où il est créé.
Solution : injection par constructeur
La façon la plus simple d'appliquer la DI est de transmettre la dépendance via le constructeur :
class UserService(private val repository: UserRepository) { fun getUser(id: Int): User { return repository.findById(id) }}Désormais, UserService ne sait pas comment UserRepository est créé. Vous pouvez transmettre n'importe quelle implémentation — une vraie base de données, une couche factice ou un mock pour les tests.
Inversion de Contrôle (IoC)
La DI est étroitement liée au principe d'Inversion of Control (IoC). Au lieu que la classe contrôle elle-même la création de ses dépendances, le contrôle est transféré à un conteneur externe ou à une fabrique. Cela rend le système faiblement couplé et facilement extensible.
Injection Manuelle de Dépendances (Manual DI)
Pour les petits projets ou à des fins pédagogiques, on peut se passer de frameworks. Créons un simple conteneur de dépendances manuellement.
Exemple : conteneur de dépendances
// Définissons l'interface du repositoryinterface UserRepository { fun findById(id: Int): User}
// Implémentationclass UserRepositoryImpl : UserRepository { override fun findById(id: Int): User { // logique de récupération de l'utilisateur depuis la BD return User(id, "User$id") }}
// Conteneur qui assemble toutes les dépendancesclass AppContainer { val userRepository: UserRepository = UserRepositoryImpl() val userService: UserService by lazy { UserService(userRepository) }}
// Utilisationfun main() { val container = AppContainer() val service = container.userService println(service.getUser(1))}Avantages : contrôle total, pas de dépendances externes, simplicité de compréhension.
Inconvénients : à mesure que le projet grandit, le conteneur s'agrandit, il devient difficile de gérer le cycle de vie des objets et leurs scopes.
Injection de Dépendances avec Hilt
Hilt est la bibliothèque officielle de Google pour la DI dans les applications Android en Kotlin. Elle est construite sur Dagger, mais est nettement plus simple à configurer.
Configuration de Hilt
Ajoutez dans build.gradle.kts (niveau projet) :
plugins { id("com.google.dagger.hilt.android") version "2.48" apply false}Dans le build.gradle.kts du module de l'application :
plugins { id("com.google.dagger.hilt.android")}
dependencies { implementation("com.google.dagger:hilt-android:2.48") kapt("com.google.dagger:hilt-compiler:2.48")