Android Coden
Android 5 min lesen

Der Zweck von Dependency Injection in Android

Dependency Injection entkoppelt Objekte von ihren Abhängigkeiten. So bleibt Code testbar und leicht wartbar.

Wer Android-Apps schreibt, begegnet früh einem stillen Problem: Klassen brauchen andere Klassen. Ein Repository braucht eine Datenbank, ein ViewModel braucht ein Repository, ein Service braucht einen HTTP-Client. Wie diese Abhängigkeiten entstehen und von wo sie kommen, entscheidet darüber, wie testbar, wartbar und erweiterbar eine App langfristig ist. Dependency Injection beantwortet diese Frage systematisch – und ist in der modernen Android-Architektur kein optionales Extra, sondern eine Grundvoraussetzung für professionellen Code.

Was ist das?

Dependency Injection (kurz DI) ist ein Design-Muster, bei dem eine Klasse ihre Abhängigkeiten nicht selbst erzeugt, sondern sie als Parameter von außen erhält. Statt tief im Konstruktor val db = Room.databaseBuilder(context, AppDatabase::class.java, "db").build() aufzurufen, bekommt die Klasse ein fertiges AppDatabase-Objekt übergeben.

Das klingt nach einer kleinen Verschiebung, hat aber weitreichende Konsequenzen: Eine Klasse erklärt nur noch, was sie braucht – nicht, wie sie es bekommt. Die Verantwortung für Konstruktion und Lebenszeit der Abhängigkeiten liegt bei einer übergeordneten Instanz, dem sogenannten Dependency Container.

Googles Architecture Guide hält fest, dass Abhängigkeiten in Android immer von einer höheren Ebene in die niedrigere injiziert werden sollen: vom Application-Scope in den Activity-Scope, von dort in ViewModels und Repositories. Dieses Prinzip nennt sich Inversion of Control – nicht das Objekt selbst, sondern eine externe Instanz kontrolliert seine Abhängigkeiten.

Wie funktioniert es?

Grundlage jeder DI-Lösung ist der Dependency Graph: die vollständige Karte davon, welche Klasse welche andere benötigt und in welcher Reihenfolge Objekte gebaut werden müssen. Ohne Framework pflegst du diesen Graphen von Hand; mit Hilt übernimmt der Compiler das zur Build-Zeit.

Hilt – der Standard in modernem Android

Hilt basiert auf Dagger und ist die von Google empfohlene Lösung. Du annotierst Klassen und ihre Einstiegspunkte, der Rest ist generierter Code:

@HiltAndroidApp
class MyApp : Application()

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var orderRepository: OrderRepository
}

@Singleton
class OrderRepository @Inject constructor(
    private val db: AppDatabase,
    private val api: OrderApiService
) {
    suspend fun loadOrders() = db.orderDao().getAll()
}

Hilt erkennt den Graphen, baut AppDatabase und OrderApiService zuerst, reicht sie an OrderRepository weiter, und injiziert das fertige Repository in die Activity. Du rufst nirgendwo manuell OrderRepository(...) auf.

Scopes und Lebensdauer

DI-Frameworks verwalten, wie lange ein Objekt lebt. @Singleton bedeutet: eine Instanz für die gesamte Laufzeit der App. @ActivityScoped bedeutet: neue Instanz pro Activity, automatisch freigegeben wenn die Activity stirbt. Falsche Scopes sind eine häufige Ursache für Memory Leaks – DI macht diese Entscheidung explizit und nachvollziehbar.

Constructor Injection vs. Field Injection

Constructor Injection ist immer vorzuziehen: Alle Abhängigkeiten stehen im Konstruktor, die Klasse ist ab dem ersten Moment vollständig initialisiert. Field Injection mit @Inject lateinit var ist nur dort nötig, wo Android selbst den Konstruktor aufruft – also in Activity, Fragment und Service. In ViewModels, Repositories und Use Cases immer Constructor Injection verwenden.

In der Praxis

Testbarkeit ist das eigentliche Versprechen

Der entscheidende Vorteil von DI liegt im Testing. Wenn eine Klasse ihre Abhängigkeiten als Parameter erhält, kannst du in Tests einfach eine andere Implementierung einsetzen – ohne Produktionscode anzufassen:

// Produktions-Aufruf (durch Hilt)
val viewModel = OrderViewModel(realOrderRepository)

// Unit-Test
val fakeRepository = FakeOrderRepository(
    orders = listOf(Order(id = 1, name = "Testbestellung"))
)
val viewModel = OrderViewModel(fakeRepository)
viewModel.loadOrders()

assertThat(viewModel.uiState.value.orders).hasSize(1)

FakeOrderRepository gibt vordefinierte Daten zurück – kein Netzwerk, keine Datenbank, kein Timing-Problem. Tests laufen in Millisekunden und sind vollständig deterministisch. Genau das beschreiben Googles Testing Fundamentals als Ziel: schnelle, isolierte Unit-Tests, die zuverlässig im CI laufen.

DI und Compose

In Jetpack Compose holst du ein Hilt-ViewModel mit hiltViewModel() statt viewModel(). Hilt injiziert alle Abhängigkeiten, bevor das ViewModel genutzt wird:

@Composable
fun OrderScreen(
    viewModel: OrderViewModel = hiltViewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // ...
}

Die Composable bleibt dabei völlig frei von Konstruktions-Logik. Dieser klare Schnitt zwischen Darstellung und Datenversorgung ist einer der Gründe, warum Compose-Apps einfacher zu testen sind als ältere View-basierte Architekturen.

Typische Stolperfalle: Abhängigkeiten intern erzeugen

Der häufigste DI-Fehler bei Einsteigern sieht so aus:

class OrderViewModel : ViewModel() {
    // FALSCH – fest verdrahtet, nicht austauschbar
    private val repository = OrderRepository(
        AppDatabase.getInstance(applicationContext)
    )
}

Dieses ViewModel ist untrennbar mit einer konkreten Datenbankinstanz verbunden. Willst du es testen, brauchst du eine echte Datenbank – oder du musst den Code ändern. Mit Constructor Injection löst sich das Problem vollständig: Der Test übergibt eine Fake-Implementierung, die Produktionsumgebung bekommt die echte.

Fazit

Dependency Injection ist keine Bibliothek und kein Framework – es ist ein Denkprinzip. Objekte sagen, was sie brauchen; jemand anderes baut es und reicht es herein. Das entkoppelt Klassen voneinander, macht Abhängigkeiten sichtbar und öffnet die Tür für schnelle, stabile Unit-Tests. Schau dir heute eine Klasse in deinem Projekt an, die mit SomeClass() direkt eine Abhängigkeit erzeugt. Refaktoriere sie auf Constructor Injection, schreib einen Test mit einer Fake-Implementierung, und beobachte, wie viel einfacher das Testen auf einmal wird – dieser Moment ist der beste Beweis dafür, warum DI in modernem Android-Code keine Option ist, sondern Standard.

Quellen (6)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.