Abhängigkeitsinjektion (DI) ist eine Technik, bei der ein Objekt seine Abhängigkeiten von außen erhält, anstatt sie selbst zu erstellen. DI ist eine Implementierung des IoC-Prinzips (Inversion of Control) und liegt Dagger, Hilt und Swinject zugrunde. Die Abhängigkeitsinjektion verringert die Codekopplung, vereinfacht Tests und macht die Architektur flexibel. Auf Android ist DI mit Dagger Hilt von Google Standard, auf iOS mit Swinject oder manueller Injektion. Weitere Informationen finden Sie im Android DI Guide.
Wichtige Punkte
Abhängigkeitsinjektion ist eine Technik, bei der ein Objekt Abhängigkeiten (Dienste, Repositories, Konfigurationen) über einen Konstruktor, Setter oder eine Schnittstelle erhält, anstatt sie selbst mit new zu erstellen. Das Ziel von DI ist es, die Kopplung zwischen Klassen zu reduzieren. Wenn eine Klasse Abhängigkeiten selbst erstellt, ist sie stark an konkrete Implementierungen gebunden, was Tests und Änderungen erschwert. Mit DI arbeitet die Klasse mit einer Abstraktion (Protokoll/Schnittstelle), und die konkrete Implementierung wird von außen bereitgestellt.
Drei Arten der Injektion — Konstruktorinjektion (über init/constructor), Setter-Injektion (über Eigenschaft/Setter), Schnittstelleninjektion (über eine Schnittstellenmethode). Die Konstruktorinjektion ist die bevorzugte Methode: Abhängigkeiten sind in der Signatur klar sichtbar, und das Objekt wird immer in einem gültigen Zustand erstellt. Die Setter-Injektion wird für optionale Abhängigkeiten mit Standardwerten verwendet. Die Schnittstelleninjektion ist selten und wird hauptsächlich für DI-Container verwendet.
| DI-Art | Methode | Wann verwenden | Beispiel |
|---|---|---|---|
| Konstruktor | Initialisierer-Parameter | Erforderliche Abhängigkeiten | init(service: ServiceProtocol) |
| Eigenschaft | Klasseneigenschaft | Optionale Abhängigkeiten | var service: ServiceProtocol? |
| Methode | Methodenparameter | Temporäre Abhängigkeiten | func doWork(with service: Service) |
DI-Container — eine Bibliothek, die die Erstellung und den Lebenszyklus von Abhängigkeiten verwaltet. Der Container enthält Typregistrierungen (jeder abstrakte Typ ist einer konkreten Implementierung zugeordnet) und eine Factory zum Erstellen von Objekten mit aufgelösten Abhängigkeiten. Auf Android — Dagger/Hilt, auf iOS — Swinject, Needle, Dip. Der Container kann den Gültigkeitsbereich (Scope) verwalten: Singleton (eine Instanz pro Anwendung), Feature-Scope (pro Bildschirm) oder ein neues Objekt bei jeder Anfrage.
Dagger Hilt ist ein Wrapper um Dagger von Google, die Standard-DI-Bibliothek für Android. Hilt vereinfacht Dagger: Es entfernt die manuelle Komponentenerstellung und fügt @HiltAndroidApp, @AndroidEntryPoint und @Module hinzu. Hilt integriert sich in den Android-Lebenszyklus: ViewModel, Activity, Fragment, Service, BroadcastReceiver können über Annotationen Abhängigkeiten erhalten. Die Codegenerierung erfolgt zur Kompilierzeit — Dagger generiert Komponentenimplementierungen, was eine Null-Runtime-Overhead bedeutet.
// Anwendungsklasse
@HiltAndroidApp
class MyApp : Application()
// Modul — definiert, wie Abhängigkeiten erstellt werden
@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 erhält Abhängigkeit über Konstruktor
@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 aktiviert DI
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
}
Dagger-Komponenten und Gültigkeitsbereiche — @Singleton (gesamte Anwendung), @ActivityScoped (pro Activity), @FragmentScoped (pro Fragment), @ViewModelScoped (pro ViewModel). Die Wahl des Gültigkeitsbereichs bestimmt die Lebensdauer des Objekts. @Singleton — eine Instanz pro Prozess, geeignet für OkHttpClient und Datenbanken. @ActivityScoped — das Objekt lebt, solange die Activity lebt, für bildschirmbezogene Abhängigkeiten. @ViewModelScoped — neu in Hilt 2.45+, das Objekt lebt, solange das ViewModel lebt, praktisch für Coroutine-Scopes.
Swinject ist ein beliebtes Open-Source-DI-Framework für iOS. Swinject bietet Container, Assemblies und verschiedene Gültigkeitsbereiche. Im Gegensatz zu Dagger arbeitet Swinject zur Laufzeit — Abhängigkeiten werden dynamisch ohne Codegenerierung aufgelöst. Das macht Swinject einfacher einzurichten, aber das Debuggen ist schwieriger: Ein Fehler einer nicht aufgelösten Abhängigkeit erscheint nur zur Laufzeit. Swinject unterstützt Konstruktorinjektion, Eigenschaftsinjektion und Methodeninjektion.
import Swinject
// Assembly — eine Gruppe von Registrierungen
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 über Konstruktorinjektion
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
}
}
}
// DI-Einrichtung in AppDelegate oder App
let assembler = Assembler([NetworkAssembly(), ServiceAssembly()])
let viewModel = assembler.resolver.resolve(ProfileViewModel.self)!
// Eigenschaftsinjektion für UIKit ViewController
container.register(UserViewController.self) { r in
let vc = UserViewController()
vc.viewModel = r.resolve(ProfileViewModel.self)
return vc
}
Swinject-Gültigkeitsbereiche — .transient (jedes Mal ein neues Objekt), .container (Singleton pro Container), .graph (Standard — das Objekt wird innerhalb eines Abhängigkeitsgraphen geteilt). Für iOS-Anwendungen sind .container und .transient ausreichend. Swinject unterstützt auch Assembler — die Gruppierung von Assembly für eine modulare Architektur. Für Tests wird Assembly durch MockAssembly ersetzt, was die Substitution von Abhängigkeiten ohne Änderung des Produktionscodes ermöglicht.
DI vs Service Locator — beide Muster lösen das Problem der Abhängigkeitsverwaltung, aber auf unterschiedliche Weise. DI injiziert Abhängigkeiten in das Objekt; Service Locator bietet ein globales Register, aus dem das Objekt selbst Abhängigkeiten anfordert. DI deklariert Abhängigkeiten explizit über den Konstruktor (oder Setter). Service Locator versteckt Abhängigkeiten — sie werden innerhalb der Methode angefordert, was die Signatur weniger informativ macht. DI ist einfacher zu testen: Es reicht aus, einen Mock an den Konstruktor zu übergeben. Service Locator erfordert die Einrichtung des globalen Registers für jeden Test.
| Eigenschaft | Abhängigkeitsinjektion | Service Locator | Manuelle Injektion |
|---|---|---|---|
| Sichtbarkeit der Abhängigkeiten | Im Konstruktor | Im Methodenrumpf versteckt | Explizit |
| Tests | Mock im Konstruktor | Locator-Einrichtung | Mock im Konstruktor |
| Einrichtungskomplexität | Erfordert DI-Container | Globales Register | Manuelle Erstellung |
| Runtime-Overhead | Dagger — Kompilierzeit | Runtime-Suche | Keiner |
DI vs manuelle Injektion — ohne DI-Container werden Abhängigkeiten manuell in Fabriken oder im AppDelegate erstellt. Für 5-10 Klassen ist die manuelle Injektion einfacher — kein Erlernen von Dagger oder Swinject erforderlich. Bei 50+ Klassen wird die manuelle Injektion problematisch: Konstruktoren mit 5-6 Parametern, komplexe Erstellungsreihenfolge, Code-Duplizierung. Ein DI-Container automatisiert diese Prozesse und bietet eine klare Lebenszyklusverwaltung. Die manuelle Injektion ohne Container ist eine gute Wahl für kleine Projekte und Prototypen.
Konstruktorinjektion — Standard. Verwenden Sie für erforderliche Abhängigkeiten immer die Konstruktorinjektion. Dies macht Abhängigkeiten explizit und das Objekt ist immer einsatzbereit. Setter-Injektion — nur für optionale Abhängigkeiten (z. B. delegate oder listener). Schnittstelleninjektion — verwenden Sie diese nicht, es sei denn, Sie schreiben Ihre eigene DI-Bibliothek. Die Konstruktorinjektion ist die einzige Möglichkeit, sicherzustellen, dass ein Objekt in einem gültigen Zustand erstellt wird.
Eine Klasse — eine Verantwortung. Wenn der Konstruktor einer Klasse 5+ Parameter erfordert, verstößt die Klasse wahrscheinlich gegen das Single-Responsibility-Prinzip. Teilen Sie die Klasse in mehrere mit weniger Abhängigkeiten auf. Ein Anzeichen: Wenn Sie eine ServiceManager-Klasse mit 6 verschiedenen Diensten schreiben — das ist das God-Object-Antipattern. Lagern Sie die Geschäftslogik in Use Cases (Interactors) aus, jeder mit 1-2 Abhängigkeiten.
// ❌ Schlecht: 6 Abhängigkeiten — 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
)
// ✅ Gut: Use Cases mit 1-2 Abhängigkeiten
class ProfileViewModel @Inject constructor(
private val loadProfileUseCase: LoadProfileUseCase,
private val trackAnalyticsUseCase: TrackAnalyticsUseCase
)
Gültigkeitsbereich und Lebenszyklus — wählen Sie den richtigen Gültigkeitsbereich für jede Abhängigkeit. Singletons: OkHttpClient, Datenbank, SharedPreferences. Feature-Scope: Repositories, Use Cases (wenn sie zustandslos sind). Transient: Value Objects, DateFormatter, Parser. Fehler bei der Bereichsfestlegung sind ein häufiges Problem: Ein Singleton, das den Bildschirmzustand speichert, führt zu Speicherlecks. Auf Android löst Hilt's @ActivityScoped dieses Problem; in Swinject verwenden Sie .container mit Vorsicht.
Häufig gestellte Fragen
new erzeugt eine starke Kopplung zwischen Klassen — Sie können die Implementierung nicht ohne Codeänderung austauschen. Tests werden schwierig: Sie können keinen Mock anstelle eines echten Dienstes injizieren. SRP wird verletzt: Die Klasse ist sowohl für die Geschäftslogik als auch für die Erstellung von Abhängigkeiten verantwortlich. DI löst diese Probleme, indem es Abhängigkeiten von außen injiziert und mit Abstraktionen arbeitet.
Dagger Hilt ist der Standard von Google, Kompilierzeit-DI mit Codegenerierung, besserer Leistung und Jetpack-Integration. Koin ist Laufzeit-DI, einfacher einzurichten, aber langsamer und mit Laufzeitfehlern. Wählen Sie Hilt für Produktionsprojekte. Koin eignet sich für Prototypen und kleine Anwendungen.
Nein. Für iOS sind verfügbar: Swinject (Laufzeit, beliebt), Needle (Kompilierzeit von Uber), Dip (leichtgewichtig), Weaver (Sourcery-basiert). Apple bietet keinen integrierten DI-Container, aber die manuelle Injektion über init ist Standardpraxis. Für SwiftUI ist manuelles DI über Environment oder @StateObject ohne externe Bibliotheken oft ausreichend.
Ja. Die manuelle Injektion über den Konstruktor ist DI ohne Framework. Service Locator ist eine Alternative ohne Framework. Fabriken und Factory Method sind ebenfalls Formen von DI. Ein Framework (Dagger, Swinject) automatisiert die routinemäßige Registrierung und Auflösung von Abhängigkeiten, aber für 10-20 Klassen ist manuelles DI ausreichend.
DI ist eine Technik (Vorlage), die das Prinzip der Inversion of Control implementiert. Im Gegensatz zu GoF-Mustern hat DI keine strenge 3-4-Klassen-Struktur. DI ist eine Möglichkeit, Abhängigkeiten zu organisieren, kein Entwurfsmuster. DI-Container (Dagger, Swinject) sind Frameworks, die diese Technik automatisieren.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch