Android Coden
Android 4 min lesen

Assisted Injection: Laufzeitparameter sauber einschleusen

Manche Abhängigkeiten kennt Hilt erst zur Laufzeit. Assisted Injection verbindet injizierte Klassen mit dynamischen Parametern über typsichere Factories.

Dependency Injection vereinfacht die Objekterzeugung erheblich – solange alle benötigten Parameter beim App-Start oder zur Kompilierzeit bekannt sind. Sobald eine Klasse aber einen Wert braucht, der erst zur Laufzeit feststeht – eine articleId aus dem Navigation-Backstack, eine chatRoomId vom Server oder eine userId aus dem Login-Flow – stößt klassische DI an ihre Grenzen. Assisted Injection ist das Muster, das genau diese Lücke schließt, ohne auf Workarounds wie versteckte SavedStateHandle-Einträge oder globale Singletons zurückgreifen zu müssen.

Was ist das?

Assisted Injection beschreibt eine Technik, bei der eine Klasse zwei Kategorien von Abhängigkeiten besitzt: solche, die der DI-Container kennt und selbst bereitstellt (z. B. ein Repository oder ein UseCase), und solche, die der Aufrufer zur Laufzeit übergeben muss (z. B. eine Datensatz-ID aus der Navigation). Die zweite Kategorie nennt man Laufzeitparameter oder „assisted parameters”.

Das Muster kombiniert beide Kategorien über eine Factory-Schnittstelle: Das DI-Framework übernimmt die statischen Abhängigkeiten, der Aufrufer liefert die dynamischen Werte. Das Ergebnis ist eine schlanke Factory-Methode, die nur die zur Laufzeit bekannten Parameter entgegennimmt – alles andere bleibt unsichtbar im DI-Graphen.

In der Android-Welt taucht dieses Problem besonders häufig bei ViewModels auf. Ein DetailViewModel benötigt typischerweise sowohl ein Repository (injizierbar) als auch die ID des anzuzeigenden Datensatzes (erst zur Laufzeit bekannt). Ohne Assisted Injection landet die ID oft im SavedStateHandle, was technisch funktioniert, aber den Typ versteckt, Null-Checks erzwingt und das isolierte Testen erschwert.

Wie funktioniert es?

Hilt unterstützt Assisted Injection mit drei Annotationen:

  • @AssistedInject – markiert den Konstruktor der Klasse, die Laufzeitparameter benötigt.
  • @Assisted – markiert jeden Parameter, der nicht vom DI-Container, sondern vom Aufrufer stammt.
  • @AssistedFactory – markiert eine innere Schnittstelle; Hilt generiert deren Implementierung automatisch zur Kompilierzeit.

Der Ablauf ist geradlinig: Hilt liest @AssistedFactory und erzeugt eine konkrete Klasse, die das Interface implementiert. Diese Factory-Implementierung kann wie jede andere Abhängigkeit injiziert werden. Der Aufrufer ruft die Factory-Methode mit dem Laufzeitparameter auf; Hilt erzeugt daraufhin die Zielklasse, füllt alle injizierten Parameter selbst und übergibt die assisted Parameter direkt vom Aufrufer.

Für ViewModels gibt es seit Hilt 2.49 zusätzlich @HiltViewModel(assistedFactory = …). Damit entfällt die manuelle ViewModelProvider.Factory-Infrastruktur vollständig; die Compose-Extension hiltViewModel(creationCallback = { … }) nimmt die typsichere Factory direkt entgegen.

Parametertransfer auf einen Blick

Compose-Screen        →  Factory.create(id)  →  ViewModel(id, repository)
                                 ↑                        ↑
                         Aufrufer liefert         Hilt injiziert

In der Praxis

Angenommen, deine App zeigt Artikel-Details an und die articleId stammt aus dem Navigation-Argument:

@HiltViewModel(assistedFactory = ArticleDetailViewModel.Factory::class)
class ArticleDetailViewModel @AssistedInject constructor(
    @Assisted val articleId: Long,
    private val repository: ArticleRepository
) : ViewModel() {

    val article = repository.getArticle(articleId).stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = null
    )

    @AssistedFactory
    interface Factory {
        fun create(articleId: Long): ArticleDetailViewModel
    }
}

Im Composable übergibst du die ID direkt über den creationCallback:

@Composable
fun ArticleDetailScreen(articleId: Long) {
    val viewModel: ArticleDetailViewModel = hiltViewModel(
        creationCallback = { factory: ArticleDetailViewModel.Factory ->
            factory.create(articleId)
        }
    )
    // UI nutzt viewModel.article
}

Typische Stolperfalle: fehlende @Assisted-Annotation

Vergisst du @Assisted bei einem Laufzeitparameter, versucht Hilt diesen Parameter wie einen normalen injizierbaren Typ aufzulösen. Da ein primitiver Long-Wert nicht im DI-Graphen registriert ist, bricht der Build mit einem aussagekräftigen Hilt-Fehler ab – das ist günstig. Gefährlicher ist die umgekehrte Variante: Wenn du einen qualifizierten String-Parameter versehentlich ohne @Assisted lässt, injiziert Hilt stillschweigend den gebundenen String-Wert statt dem Laufzeitwert, ohne Compile-Fehler.

Faustregel: Schreibe in @AssistedFactory nur die Parameter auf, die zur Laufzeit bekannt sind, und stelle sicher, dass Typ und Reihenfolge in Konstruktor und Factory-Methode exakt übereinstimmen. Bei mehreren assisted Parametern desselben Typs (z. B. zwei String-IDs) musst du @Assisted("qualifier") mit einem eindeutigen Namen einsetzen.

Testen der Factory

Ein wesentlicher Vorteil gegenüber SavedStateHandle-Ansätzen ist die Testbarkeit. Im Unit-Test kannst du die Factory direkt instanziieren, ohne Hilt, ohne Instrumented Test:

@Test
fun `viewModel exposes correct articleId`() {
    val fakeRepo = FakeArticleRepository()
    val factory = object : ArticleDetailViewModel.Factory {
        override fun create(articleId: Long) =
            ArticleDetailViewModel(articleId, fakeRepo)
    }
    val vm = factory.create(42L)
    assertThat(vm.articleId).isEqualTo(42L)
}

Kein Mocking des DI-Containers, kein SavedStateHandle-Setup – nur pure Logik, die schnell im JVM-Test läuft. Genau diese Eigenschaft zählt die offizielle Android-Architektur-Dokumentation als einen der Hauptvorteile sauberer Abhängigkeitsverwaltung.

Fazit

Assisted Injection ist kein Spezialwerkzeug für Randfälle, sondern ein alltäglicher Baustein in produktiven Android-Apps. Sobald ein ViewModel oder eine Use-Case-Klasse einen Laufzeitparameter benötigt, ist @AssistedInject die sauberste Lösung: typsicher, testbar, gut lesbar – und ohne Umwege über globale Zustände. Schaue in deinem aktuellen Projekt nach, ob irgendwo IDs über SavedStateHandle.get<Long>("id") hereinkommen, die sich als assistierte Parameter modellieren ließen. Refaktoriere einen solchen ViewModel, schreibe anschließend einen Unit-Test für die Factory und führe die App danach im Emulator aus – dieser Dreischritt macht das Muster schneller greifbar als jedes Lesen allein.

Quellen (5)
Redaktion

Geschrieben von

Redaktion

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