Hilt Grundlagen: Dependency Injection in Android
Hilt vereinfacht DI in Android durch generierte Dagger-Komponenten. Du lernst Module, Scopes und Annotationen wie @Inject kennen.
Dependency Injection klingt sofort einleuchtend, versinkt in der Praxis aber schnell in Boilerplate-Code. Hilt, Googles offizielle DI-Bibliothek für Android, löst genau dieses Problem: Es baut auf Dagger auf, kennt aber bereits den Android-Lebenszyklus und generiert den nötigen Komponentencode zur Compile-Zeit. Als Baustein der Modern Android Architecture ist Hilt in Phase 6 der Roadmap das Werkzeug, das saubere Architektur und echte Testbarkeit erst greifbar macht.
Was ist das?
Dependency Injection bedeutet: Ein Objekt bekommt seine Abhängigkeiten von außen, anstatt sie selbst zu erzeugen. Statt val repo = UserRepository(ApiService()) in einer Activity zu schreiben, erklärt du Hilt, was du brauchst, und Hilt liefert die fertige Instanz – vollständig konfiguriert und mit korrekter Lebensdauer.
Hilt ist nicht das erste DI-Framework für Android, aber das erste, das den Android-Komponentenbaum als erstklassiges Konzept kennt. Es liefert vorgefertigte Komponenten für Application, Activity, Fragment, View, Service und BroadcastReceiver. Du musst diese Hierarchie nicht selbst aufbauen: Hilt generiert sie zur Compile-Zeit aus deinen Annotationen.
Gegenüber manuellem Dagger entfällt das Schreiben von @Component-Interfaces und @Subcomponent-Definitionen. Gegenüber Service-Locator-Ansätzen bleiben die Abhängigkeiten explizit im Konstruktor deklariert – lesbar, refaktorierbar und vor allem testbar.
Wie funktioniert es?
Hilt baut auf fünf zentralen Annotationen auf, die zusammen den vollständigen DI-Graphen beschreiben.
@HiltAndroidApp
Diese Annotation an deiner Application-Klasse ist der Auslöser für die gesamte Codegenerierung. Hilt erzeugt daraufhin einen App-weiten Komponentenbaum, der als Wurzel aller weiteren Komponenten dient.
@HiltAndroidApp
class MyApp : Application()
Ohne @HiltAndroidApp kompiliert dein Projekt, aber alle anderen Hilt-Annotationen haben keine Wirkung.
@AndroidEntryPoint
Diese Annotation öffnet eine Android-Klasse für die Injektion. Hilt generiert eine Superklasse, die den zugehörigen Komponentenknoten kennt und annotierte Felder befüllt.
@AndroidEntryPoint
class UserActivity : AppCompatActivity() { ... }
@Inject
Steht @Inject vor einem Konstruktor, weiß Hilt, wie es die Klasse instanziieren soll. Steht es vor einem Feld in einer Activity oder einem Fragment, befüllt Hilt das Feld beim Erstellen der Klasse.
class UserRepository @Inject constructor(
private val apiService: ApiService
)
@Module und @InstallIn
Manche Abhängigkeiten kann Hilt nicht durch Konstruktorinjektion auflösen – externe Bibliotheken ohne @Inject-Konstruktor wie Retrofit oder Room gehören dazu. Module bündeln diese Bereitstellungslogik. @InstallIn legt fest, in welcher Hilt-Komponente das Modul lebt und damit auch, wie lange seine Objekte überleben.
Scopes
Ohne Scope erzeugt Hilt bei jeder Injektion eine neue Instanz. Scopes wie @Singleton (Application-Lebensdauer) oder @ActivityScoped (Activity-Lebensdauer) sorgen für Wiederverwendung. @ViewModelScoped bindet ein Objekt an einen ViewModel, was besonders beim Navigieren zwischen Fragments nützlich ist.
In der Praxis
Ein typischer Hilt-Stack in einer Android-App sieht so aus:
// 1. Application
@HiltAndroidApp
class MyApp : Application()
// 2. Netzwerk-Modul für Retrofit (kein @Inject-Konstruktor möglich)
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideApiService(): ApiService =
Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
// 3. Repository mit Konstruktorinjektion
class UserRepository @Inject constructor(
private val apiService: ApiService
) {
suspend fun getUser(id: String) = apiService.fetchUser(id)
}
// 4. ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel()
// 5. Activity
@AndroidEntryPoint
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
}
Hilt schließt alle Lücken automatisch: ApiService kommt aus NetworkModule, fließt in UserRepository, wird von UserViewModel übernommen – und die Activity bekommt den ViewModel per viewModels() ohne manuelle Factory.
Typische Stolperfallen
Feldinjektionen zu früh lesen: Injizierte Felder in Activities stehen erst nach super.onCreate() zur Verfügung. Greifst du in init {} oder vor dem Super-Aufruf darauf zu, erhältst du eine NullPointerException. Hilt befüllt Felder innerhalb der generierten super.onCreate()-Implementierung – das ist kein Zufall, sondern Teil der Komponentenarchitektur.
Fehlendes @InstallIn: Vergisst du @InstallIn an einem Modul, kompiliert der Code, aber Hilt installiert das Modul in keine Komponente. Das Ergebnis ist ein Build-Fehler [Dagger/MissingBinding] – manchmal mit langer Fehlerkette. Prüfe immer, ob beide Annotationen vorhanden sind.
Falscher Scope: Ein Repository ohne Scope wird bei jeder Injektion neu erzeugt. Bei zustandsbehafteten Repositories führt das zu schwer nachvollziehbaren Bugs, weil zwei Stellen im Code unterschiedliche Instanzen halten. Überlege für jede Abhängigkeit bewusst, welche Lebensdauer sinnvoll ist, bevor du den Scope festlegst.
Fazit
Hilt macht Dependency Injection in Android beherrschbar: Der Compiler prüft alle Bindings, der Komponentenbaum ist vorgegeben, und Scopes machen Lebensdauern explizit statt implizit. Die beste Art, das Gelernte zu festigen, ist ein echter Einbau in ein kleines Projekt – injiziere ein Repository in ein ViewModel, ersetze es im Unit-Test durch eine Fake-Implementierung mit @UninstallModules und @TestInstallIn, und lies das Build-Log, wenn du absichtlich eine Abhängigkeit weglässt. Wenn du verstehst, warum der Compiler in diesem Moment abbricht, hast du das Komponentenmodell von Hilt wirklich verinnerlicht.