Injection de dépendances (DI) est une technique par laquelle un objet reçoit ses dépendances de l'extérieur plutôt que de les créer lui-même. La DI est une implémentation du principe IoC (Inversion de Contrôle) et est à la base de Dagger, Hilt et Swinject. L'injection de dépendances réduit le couplage du code, simplifie les tests et rend l'architecture flexible. Sous Android, la DI est standard via Dagger Hilt de Google ; sous iOS, via Swinject ou l'injection manuelle. En savoir plus dans le Guide DI Android.
Points clés
Injection de dépendances est une technique par laquelle un objet reçoit des dépendances (services, dépôts, configurations) via un constructeur, un setter ou une interface, plutôt que de les créer lui-même avec new. Le but de la DI est de réduire le couplage entre les classes. Si une classe crée elle-même ses dépendances, elle est fortement liée à des implémentations spécifiques, ce qui rend les tests et les modifications difficiles. Avec la DI, la classe travaille avec une abstraction (protocole/interface) et l'implémentation concrète est fournie de l'extérieur.
Trois méthodes d'injection — Injection par constructeur (via init/constructor), Injection par setter (via propriété/setter), Injection par interface (via une méthode d'interface). L'injection par constructeur est la méthode préférée : les dépendances sont clairement visibles dans la signature et l'objet est toujours créé dans un état valide. L'injection par setter est utilisée pour les dépendances optionnelles avec valeur par défaut. L'injection par interface est rare, principalement pour les conteneurs DI.
| Type de DI | Méthode | Quand l'utiliser | Exemple |
|---|---|---|---|
| Constructeur | Paramètres de l'initialiseur | Dépendances obligatoires | init(service: ServiceProtocol) |
| Propriété | Propriété de classe | Dépendances optionnelles | var service: ServiceProtocol? |
| Méthode | Paramètre de méthode | Dépendances temporaires | func doWork(with service: Service) |
Conteneur DI — une bibliothèque qui gère la création et le cycle de vie des dépendances. Le conteneur contient des enregistrements de types (chaque type abstrait mappé à une implémentation concrète) et une fabrique pour créer des objets avec des dépendances résolues. Sous Android — Dagger/Hilt, sous iOS — Swinject, Needle, Dip. Le conteneur peut gérer la portée : singleton (une instance par application), portée de fonctionnalité (par écran) ou un nouvel objet à chaque demande.
Dagger Hilt est une surcouche de Dagger de Google, la bibliothèque DI standard pour Android. Hilt simplifie Dagger : il supprime la création manuelle de composants, ajoute @HiltAndroidApp, @AndroidEntryPoint et @Module. Hilt s'intègre au cycle de vie Android : ViewModel, Activity, Fragment, Service, BroadcastReceiver peuvent recevoir des dépendances via des annotations. La génération de code se produit à la compilation — Dagger génère des implémentations de composants, ce qui donne une surcharge d'exécution nulle.
// Classe d'application
@HiltAndroidApp
class MyApp : Application()
// Module — définit comment créer les dépendances
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()
@Provides
@Singleton
fun provideApiService(client: OkHttpClient): ApiService {
return Retrofit.Builder()
.baseUrl("https://api.example.com")
.client(client)
.build()
.create(ApiService::class.java)
}
}
// ViewModel reçoit la dépendance via le constructeur
@HiltViewModel
class MainViewModel @Inject constructor(
private val apiService: ApiService
) : ViewModel() {
private val _state = MutableStateFlow(MainState.Loading)
val state: StateFlow<MainState> = _state.asStateFlow()
fun loadData() {
viewModelScope.launch {
_state.value = MainState.Success(apiService.getData())
}
}
}
// Activity — @AndroidEntryPoint active la DI
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
}
Composants et portées Dagger — @Singleton (toute l'application), @ActivityScoped (par Activity), @FragmentScoped (par Fragment), @ViewModelScoped (par ViewModel). Le choix de la portée détermine la durée de vie de l'objet. @Singleton — une instance par processus, adapté à OkHttpClient et aux bases de données. @ActivityScoped — l'objet vit tant que l'Activity vit, pour les dépendances au niveau de l'écran. @ViewModelScoped — nouveauté de Hilt 2.45+, l'objet vit tant que le ViewModel vit, pratique pour les portées de coroutines.
Swinject est un framework DI open source populaire pour iOS. Swinject fournit Container, Assemblies et diverses portées. Contrairement à Dagger, Swinject fonctionne à l'exécution — les dépendances sont résolues dynamiquement sans génération de code. Cela rend Swinject plus facile à configurer, mais le débogage est plus difficile : une erreur de dépendance non résolue n'apparaît qu'à l'exécution. Swinject prend en charge l'injection par constructeur, l'injection par propriété et l'injection par méthode.
import Swinject
// Assembly — un groupe d'enregistrements
class NetworkAssembly: Assembly {
func assemble(container: Container) {
container.register(NetworkServiceProtocol.self) { _ in
NetworkService()
}.inObjectScope(.container) // singleton
container.register(UserRepositoryProtocol.self) { r in
UserRepository(
networkService: r.resolve(NetworkServiceProtocol.self)!
)
}
}
}
// ViewModel via l'injection par constructeur
class ProfileViewModel: ObservableObject {
private let repository: UserRepositoryProtocol
init(repository: UserRepositoryProtocol) {
self.repository = repository
}
@Published var user: User?
func loadUser() {
repository.fetchUser { [weak self] user in
self?.user = user
}
}
}
// Configuration de la DI dans AppDelegate ou App
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!
// Injection par propriété pour UIKit ViewController
container.register(UserViewController.self) { r in
let vc = UserViewController()
vc.viewModel = r.resolve(ProfileViewModel.self)
return vc
}
Portées Swinject — .transient (nouvel objet à chaque fois), .container (singleton par conteneur), .graph (par défaut — l'objet est partagé au sein d'un seul graphe de dépendances). Pour les applications iOS, .container et .transient suffisent. Swinject prend également en charge Assembler — le regroupement d'Assembly pour une architecture modulaire. Pour les tests, Assembly est remplacé par MockAssembly, permettant la substitution de dépendances sans modifier le code de production.
DI vs Service Locator — les deux modèles résolvent la gestion des dépendances, mais différemment. La DI injecte les dépendances dans l'objet ; Service Locator fournit un registre global à partir duquel l'objet demande lui-même les dépendances. La DI déclare explicitement les dépendances via le constructeur (ou le setter). Service Locator cache les dépendances — elles sont demandées à l'intérieur de la méthode, rendant la signature moins informative. La DI est plus facile à tester : il suffit de passer un mock au constructeur. Service Locator nécessite la configuration du registre global pour chaque test.
| Caractéristique | Injection de dépendances | Service Locator | Injection manuelle |
|---|---|---|---|
| Visibilité des dépendances | Dans le constructeur | Cachées dans le corps de la méthode | Explicites |
| Tests | Mock dans le constructeur | Configuration du Locator | Mock dans le constructeur |
| Complexité de configuration | Nécessite un conteneur DI | Registre global | Création manuelle |
| Surcharge d'exécution | Dagger — compilation | Recherche à l'exécution | Aucune |
DI vs injection manuelle — sans conteneur DI, les dépendances sont créées manuellement dans des fabriques ou AppDelegate. Pour 5 à 10 classes, l'injection manuelle est plus simple — pas besoin d'apprendre Dagger ou Swinject. Pour 50+ classes, l'injection manuelle devient problématique : constructeurs avec 5 à 6 paramètres, ordre de création complexe, duplication de code. Un conteneur DI automatise ces processus et fournit une gestion claire du cycle de vie. L'injection manuelle sans conteneur est un bon choix pour les petits projets et les prototypes.
Injection par constructeur — standard. Utilisez toujours l'injection par constructeur pour les dépendances obligatoires. Cela rend les dépendances explicites et l'objet toujours prêt à fonctionner. Injection par setter — uniquement pour les dépendances optionnelles (par exemple, delegate ou listener). Injection par interface — ne l'utilisez pas sauf si vous écrivez votre propre bibliothèque DI. L'injection par constructeur est le seul moyen de garantir qu'un objet est créé dans un état valide.
Une classe — une responsabilité. Si le constructeur d'une classe nécessite 5+ paramètres, la classe viole probablement le principe de responsabilité unique. Divisez la classe en plusieurs avec moins de dépendances. Un signe : si vous écrivez une classe ServiceManager avec 6 services différents — c'est l'anti-modèle God Object. Extrayez la logique métier dans des Use Cases (Interactors), chacun avec 1 à 2 dépendances.
// ❌ Mauvais : 6 dépendances — God Object
class ProfileViewModel @Inject constructor(
private val api: ApiService,
private val db: Database,
private val analytics: Analytics,
private val prefs: Preferences,
private val location: LocationProvider,
private val notification: NotificationManager
)
// ✅ Bon : Use Cases avec 1-2 dépendances
class ProfileViewModel @Inject constructor(
private val loadProfileUseCase: LoadProfileUseCase,
private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)
Portée et cycle de vie — choisissez la bonne portée pour chaque dépendance. Singletons : OkHttpClient, base de données, SharedPreferences. Portée de fonctionnalité : dépôts, Use Cases (s'ils sont sans état). Transient : Value Objects, DateFormatter, analyseurs. Les erreurs de portée sont un problème courant : un singleton stockant l'état de l'écran provoque des fuites. Sous Android, @ActivityScoped de Hilt résout ce problème ; dans Swinject, utilisez .container avec prudence.
Questions fréquentes
new crée un couplage fort entre les classes — vous ne pouvez pas changer l'implémentation sans modifier le code. Les tests deviennent difficiles : vous ne pouvez pas injecter un mock à la place d'un vrai service. Le SRP est violé : la classe est responsable à la fois de la logique métier et de la création des dépendances. La DI résout ces problèmes en injectant les dépendances de l'extérieur et en travaillant avec des abstractions.
Dagger Hilt est le standard de Google, DI à la compilation avec génération de code, meilleures performances et intégration Jetpack. Koin est DI à l'exécution, plus facile à configurer, mais plus lent et avec des erreurs à l'exécution. Choisissez Hilt pour les projets de production. Koin convient aux prototypes et aux petites applications.
Non. Pour iOS sont disponibles : Swinject (exécution, populaire), Needle (compilation par Uber), Dip (léger), Weaver (basé sur Sourcery). Apple ne fournit pas de conteneur DI intégré, mais l'injection manuelle via init est une pratique standard. Pour SwiftUI, la DI manuelle via Environment ou @StateObject sans bibliothèques externes est souvent suffisante.
Oui. L'injection manuelle via le constructeur est de la DI sans framework. Service Locator est une alternative sans framework. Les fabriques et Factory Method sont aussi des formes de DI. Un framework (Dagger, Swinject) automatise l'enregistrement et la résolution des dépendances, mais pour 10 à 20 classes, la DI manuelle suffit.
La DI est une technique (template) qui implémente le principe d'Inversion de Contrôle. Contrairement aux modèles GoF, la DI n'a pas de structure stricte de 3 à 4 classes. La DI est une façon d'organiser les dépendances, pas un modèle de conception. Les conteneurs DI (Dagger, Swinject) sont des frameworks qui automatisent cette technique.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi