Android Coden
Android 5 min lesen

Lifecycle-Aware Collection: Streams nur bei aktiver UI sammeln

repeatOnLifecycle koppelt deinen Flow sauber an den UI-Zustand. So sammelst du Streams nur, wenn die UI aktiv genug ist, um Updates anzuzeigen.

Flows und StateFlows haben die Art und Weise, wie Android-Apps Daten verarbeiten, grundlegend verändert. Doch ein Flow allein ist kein Garant für eine sauber laufende App: Wer einen Collector zu früh startet und ihn nie stoppt, verschwendet Ressourcen und riskiert Abstürze oder unerwünschte Hintergrundarbeit. Lifecycle-Aware Collection löst genau dieses Problem – mit zwei eleganten APIs, die den Collector automatisch an den Zustand der UI koppeln.

Was ist das?

Lifecycle-Aware Collection bezeichnet das Muster, einen Flow oder StateFlow nur dann zu beobachten, wenn die zugehörige UI tatsächlich aktiv und für den Nutzer sichtbar ist. „Aktiv genug” bedeutet in der Android-Welt: der Lifecycle-Zustand des Screens hat einen bestimmten Schwellenwert erreicht oder überschritten – typischerweise STARTED oder RESUMED.

Hinter diesem Konzept steckt Androids Lifecycle-System. Jede Activity und jedes Fragment durchläuft definierte Zustände: CREATED, STARTED, RESUMED, PAUSED, STOPPED und DESTROYED. Eine Activity ist sichtbar, solange sie mindestens STARTED ist, aber nur dann wirklich im Vordergrund und interaktiv, wenn sie RESUMED ist. Lifecycle-Aware Collection nutzt genau diese Grenzen, um Collector-Coroutinen präzise zu starten und zu stoppen.

Die beiden zentralen APIs sind repeatOnLifecycle für klassischen View-Code (Fragments, Activities) und collectAsStateWithLifecycle für Jetpack Compose. Beide setzen dasselbe Prinzip um, bieten aber eine für die jeweilige UI-Schicht optimierte Syntax. Sie sind Teil der offiziellen Android-Architekturempfehlungen und gehören heute zum Standardrepertoire moderner App-Entwicklung.

Wie funktioniert es?

repeatOnLifecycle ist eine Erweiterungsfunktion auf Lifecycle. Du rufst sie innerhalb einer Coroutine auf und übergibst den Mindest-Lifecycle-Zustand, ab dem dein Block ausgeführt werden soll:

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state ->
            updateUi(state)
        }
    }
}

Was hier passiert, ist bemerkenswert präzise: Sobald der Lifecycle STARTED erreicht, startet repeatOnLifecycle den inneren Block als neue Coroutine. Sinkt der Lifecycle wieder unter STARTED – also beim Wechsel in STOPPED, etwa wenn die App in den Hintergrund geht –, wird diese Coroutine automatisch abgebrochen. Kehrt der Lifecycle zu STARTED zurück, startet der Block erneut. Das geschieht so oft, wie es der Lifecycle erfordert, daher der Name repeat.

In Jetpack Compose ist das Äquivalent die Erweiterungsfunktion collectAsStateWithLifecycle aus dem Artifact androidx.lifecycle:lifecycle-runtime-compose:

val uiState by viewModel.uiState.collectAsStateWithLifecycle()

Intern verwendet collectAsStateWithLifecycle ebenfalls repeatOnLifecycle mit einem konfigurierbaren Zustand (Standard: STARTED). Es gibt den aktuellen Wert des Flows als Compose-State zurück, sodass die UI bei jedem neuen Wert automatisch neu zusammengesetzt wird – aber eben nur, solange die UI aktiv ist. Beide APIs verlassen sich auf das Kotlin-Coroutine-Prinzip der strukturierten Nebenläufigkeit: Der Collector lebt immer innerhalb eines klar begrenzten Scopes und wird nie versehentlich „vergessen”.

In der Praxis

Das häufigste Antipattern

Der verbreitetste Fehler sieht harmlos aus:

// FALSCH – läuft weiter, während die App im Hintergrund ist
lifecycleScope.launch {
    viewModel.uiState.collect { state ->
        updateUi(state)
    }
}

lifecycleScope lebt von CREATED bis DESTROYED einer Activity oder eines Fragments. Das bedeutet: Dieser Collector läuft weiter, während der Nutzer zur Home-Schaltfläche wechselt, einen Anruf beantwortet oder eine andere App öffnet. Du verarbeitest Daten und aktualisierst Views, die der Nutzer gar nicht sieht. Im schlimmsten Fall crasht die App, weil Views in einem ungültigen Zustand manipuliert werden, oder das ViewModel pumpt unnötig Netzwerkanfragen durch.

Richtiges Muster im Fragment

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
            viewModel.uiState.collect { state ->
                render(state)
            }
        }
    }
}

Beachte viewLifecycleOwner statt this oder lifecycleOwner. Im Fragment hat der View einen eigenen Lifecycle, der kürzer ist als der Fragment-Lifecycle selbst. Nutzt du fälschlicherweise this.repeatOnLifecycle, kann es bei Navigation-Backstack-Szenarien zu mehrfach aktiven Collectoren für dieselbe UI kommen – ein schwer zu findender Bug.

Compose-Variante

@Composable
fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    when (uiState) {
        is UiState.Loading -> LoadingIndicator()
        is UiState.Success -> ContentView((uiState as UiState.Success).data)
        is UiState.Error -> ErrorView((uiState as UiState.Error).message)
    }
}

Dies ist die empfohlene Variante aus den offiziellen Android-Architekturempfehlungen. Sie ist kompakter als collectAsState() und lifecycle-korrekt. collectAsState() besitzt diese Awareness nicht und sollte in App-Code durch collectAsStateWithLifecycle() ersetzt werden.

Mehrere Flows parallel sammeln

Wenn du mehrere Flows beobachten willst, starte mehrere launch-Calls innerhalb des wiederholten Blocks:

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        launch { viewModel.uiState.collect { render(it) } }
        launch { viewModel.events.collect { handleEvent(it) } }
    }
}

So sind beide Collector-Coroutinen an denselben Lifecycle-Schwellenwert gebunden und werden gemeinsam gestartet und gestoppt. Würdest du stattdessen zwei separate lifecycleScope.launch { repeatOnLifecycle(...) {} }-Blöcke verwenden, ist das funktional korrekt, aber redundant und schwerer lesbar.

Fazit

Lifecycle-Aware Collection ist kein optionales Feintuning – es ist ein fundamentales Sicherheitsnetz für jede App, die mit Flows und einer UI aus Fragments oder Compose arbeitet. repeatOnLifecycle und collectAsStateWithLifecycle machen es einfach, das Richtige zu tun: Sie starten Collector-Coroutinen genau dann, wenn die UI bereit ist, und beenden sie, wenn sie es nicht mehr ist. Überprüfe in deinem nächsten Code-Review jeden lifecycleScope.launch { flow.collect {} }-Block und frage dich, ob er von repeatOnLifecycle umschlossen werden sollte. Schreib einen einfachen Test, der deinen ViewModel-Flow emittiert, während dein Fragment gestoppt ist, und stelle sicher, dass kein Update die UI erreicht. Wer dieses eine Muster verinnerlicht, hebt die Zuverlässigkeit und Energieeffizienz seiner App spürbar an.

Quellen (6)
Redaktion

Geschrieben von

Redaktion

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