Lifecycle-Aware Flow Collection in Android
Du lernst, Flow-Daten nur bei aktiver UI zu sammeln. So vermeidest du unnötige Arbeit und unsichere Updates.
Lifecycle-Aware Flow Collection bedeutet: Du sammelst Werte aus einem Kotlin Flow nur dann, wenn die UI in einem Zustand ist, in dem sie diese Werte sinnvoll und sicher verarbeiten kann. Das klingt nach einem Detail, entscheidet in echten Android-Apps aber über stabile Navigation, saubere Ressourcen-Nutzung und vorhersehbares Verhalten, besonders wenn ViewModels, Coroutines, Flow und Jetpack Compose zusammenspielen.
Was ist das?
Ein Flow ist ein asynchroner Datenstrom. Er kann zum Beispiel UI-State aus einem ViewModel liefern, Suchergebnisse nach jeder Eingabe aktualisieren, Datenbankänderungen aus Room weiterreichen oder Netzwerkstatus melden. Eine UI kann diesen Strom sammeln, also collect aufrufen, und auf jede Emission reagieren. Das Problem: Android-UIs leben nicht dauerhaft. Eine Activity kann sichtbar, pausiert, gestoppt oder zerstört sein. Ein Fragment kann seine View verlieren, während das Fragment-Objekt noch existiert. Eine Compose-Oberfläche kann neu zusammengesetzt werden oder aus der Composition verschwinden.
Lifecycle-Aware Flow Collection löst genau diese Grenze zwischen Datenstrom und UI-Lebenszyklus. Du sagst nicht nur: „Sammle diesen Flow“, sondern: „Sammle ihn nur, solange die UI mindestens in einem bestimmten Lifecycle-Zustand ist.“ In klassischem View-Code ist dafür repeatOnLifecycle der zentrale Baustein. In Compose nutzt du typischerweise lifecycle-aware APIs wie collectAsStateWithLifecycle, wenn du Flow-Werte als State in der UI anzeigen willst.
Das Ziel ist Safety: Die UI soll nicht auf Werte reagieren, wenn sie nicht bereit dafür ist. Ein gestoppter Screen muss keine Textfelder aktualisieren, keine Navigation auslösen und keine Animation starten. Gleichzeitig soll die Collection automatisch wieder starten, wenn der Screen zurückkommt. Du bekommst damit ein Modell, das zum Android-System passt, statt gegen den Lifecycle zu arbeiten.
Für Lernende ist das wichtigste mentale Modell: Ein Flow produziert Werte, aber die UI hat Öffnungszeiten. Lifecycle-aware Collection koppelt die Beobachtung an diese Öffnungszeiten. Dein ViewModel darf den Zustand vorbereiten und bereitstellen. Der Screen entscheidet, wann er zuhört. Diese Trennung passt gut zu moderner Android-Architektur: ViewModels halten UI-State, Repositories liefern Datenströme, und Activities, Fragments oder Composables sammeln diese Daten nur in passenden Lebensphasen.
Wie funktioniert es?
repeatOnLifecycle ist eine suspendierende API aus Jetpack Lifecycle. Du rufst sie innerhalb einer Coroutine auf und gibst einen Zielzustand an, zum Beispiel Lifecycle.State.STARTED. Sobald der Lifecycle mindestens diesen Zustand erreicht, startet der Block. Fällt der Lifecycle darunter, wird die darin laufende Coroutine abgebrochen. Erreicht die UI den Zustand später erneut, wird der Block neu gestartet.
Das ist wichtig, weil Flow-Collection oft dauerhaft läuft. Ein collect beendet sich bei vielen UI-Flows nicht von selbst. Ein StateFlow im ViewModel bleibt aktiv, solange das ViewModel lebt. Ein Flow aus einer Datenbank kann bei jeder Änderung neue Werte liefern. Wenn du so einen Flow in einem ungeeigneten Scope sammelst, läuft Arbeit weiter, obwohl der Screen nicht sichtbar ist. Das kann unnötige CPU-Arbeit verursachen, Speicher länger halten als nötig oder UI-Aktionen zu einem falschen Zeitpunkt auslösen.
Der typische Zielzustand ist STARTED. In diesem Zustand ist die UI sichtbar oder kurz davor, aktiv bedienbar zu sein. Für reine Anzeige von UI-State reicht das meistens. RESUMED ist strenger und eignet sich eher, wenn du nur bei vollständig aktivem Screen reagieren willst, etwa bei bestimmten Kamera-, Sensor- oder Fokus-Szenarien. Für normale Datenanzeige ist STARTED die übliche Entscheidung.
In einem Fragment musst du besonders auf den richtigen Lifecycle achten. Verwende für View-bezogene Collections den viewLifecycleOwner.lifecycle, nicht den Lifecycle des Fragment-Objekts. Der Grund ist praktisch: Die View eines Fragments kann zerstört werden, während das Fragment später eine neue View erstellt. Wenn du an den falschen Lifecycle bindest, kann alter UI-Code weiterlaufen und auf Views zugreifen, die nicht mehr gültig sind.
In Compose wirkt das Thema auf den ersten Blick anders, weil du keine XML-Views aktualisierst. Trotzdem gilt derselbe Grundsatz. Ein Composable kann Flow-Werte als State sammeln und damit Recomposition auslösen. Wenn diese Collection nicht lifecycle-aware ist, kann sie weiterlaufen, obwohl der Screen im Hintergrund liegt. Mit collectAsStateWithLifecycle sammelst du Flow-Werte so, dass sie an den Lifecycle des aktuellen Owners gebunden sind. Das passt zu Compose, weil die UI aus State beschrieben wird: Der Flow liefert State, Compose rendert daraus die Oberfläche, und der Lifecycle begrenzt, wann diese Verbindung aktiv ist.
Wichtig ist auch die Unterscheidung zwischen State und Events. Ein StateFlow für Bildschirminhalte, Ladezustand und Fehlermeldungen kann gut lifecycle-aware gesammelt werden. Einzelereignisse wie Navigation, Snackbar oder einmalige Dialoge brauchen mehr Sorgfalt. Wenn ein Event während eines gestoppten Zustands entsteht, musst du bewusst entscheiden, ob es später noch relevant ist. Lifecycle-aware Collection verhindert nicht automatisch alle Event-Probleme. Sie sorgt nur dafür, dass die UI nicht außerhalb ihres sicheren Fensters reagiert.
Ein gutes Architekturbild sieht so aus: Das ViewModel besitzt einen stabilen uiState, oft als StateFlow. Es startet länger laufende Datenarbeit im viewModelScope. Die UI sammelt diesen State lifecycle-aware. Dadurch hängt die Datenquelle nicht direkt an der View, und die View sammelt nur dann, wenn sie Werte anzeigen kann. Diese Grenze macht Code leichter testbar, weil du ViewModel-Logik getrennt von Lifecycle-Fragen prüfen kannst.
In der Praxis
In klassischem Fragment-Code sieht eine lifecycle-aware Collection so aus:
class ArticleListFragment : Fragment(R.layout.article_list) {
private val viewModel: ArticleListViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
render(state)
}
}
}
}
private fun render(state: ArticleListUiState) {
// Text, Liste, Ladeanzeige und Fehlerzustand aktualisieren
}
}
Der äußere launch startet eine Coroutine im Scope der View. repeatOnLifecycle übernimmt danach das wiederholte Starten und Stoppen der eigentlichen Collection. Wenn der Screen sichtbar wird, läuft collect. Wenn der Screen gestoppt wird, wird die Collection abgebrochen. Kommt der Screen zurück, startet sie neu und bekommt bei einem StateFlow sofort den aktuellen Wert.
In Compose ist der häufige Fall kürzer:
@Composable
fun ArticleListRoute(
viewModel: ArticleListViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
ArticleListScreen(
state = uiState,
onRefresh = viewModel::refresh
)
}
Hier bleibt das Composable auf State ausgerichtet. Du startest keine manuelle Coroutine für reine Anzeige. Der State wird lifecycle-aware gesammelt und löst Recomposition aus, wenn neue Werte eintreffen. Das ist in Compose meist die passendere Form, weil du UI nicht imperativ aktualisierst, sondern aus uiState ableitest.
Eine einfache Entscheidungsregel hilft im Alltag: Wenn ein Flow direkt UI rendert, sammle ihn an der UI-Grenze lifecycle-aware. Im Fragment heißt das meist viewLifecycleOwner.repeatOnLifecycle(STARTED). In Compose heißt das meist collectAsStateWithLifecycle. Wenn ein Flow dagegen fachliche Arbeit im ViewModel verbindet, etwa Repository-Daten in UI-State umwandelt, gehört diese Arbeit eher in den viewModelScope und wird dort mit Flow-Operatoren modelliert.
Eine typische Stolperfalle ist lifecycleScope.launch { flow.collect { ... } } direkt in onCreate oder onViewCreated, ohne repeatOnLifecycle. Der Code wirkt korrekt, weil er kompiliert und anfangs funktioniert. Später zeigt sich das Problem: Die Collection bleibt aktiv, wenn der Screen nicht mehr sichtbar ist. Bei Navigation, Rotation oder Backstack-Wechseln kann das zu mehrfachen Sammlern, doppelten Snackbar-Meldungen oder Updates auf eine alte View führen. Besonders bei Fragments fällt dieser Fehler oft erst auf, wenn du schnell zwischen Screens wechselst.
Eine zweite Stolperfalle ist zu viel Arbeit im UI-Collector. Der Collector sollte nicht plötzlich große Datenmengen sortieren, lange JSON-Strukturen parsen oder Netzwerkaufrufe starten. Lifecycle-aware Collection schützt den UI-Lifecycle, ersetzt aber keine saubere Schichten-Trennung. Rechenarbeit gehört in Repository, Use Case oder ViewModel. Die UI sollte möglichst nur State entgegennehmen und darstellen.
Auch bei mehreren Flows musst du bewusst entscheiden. Du kannst mehrere Collections innerhalb desselben repeatOnLifecycle-Blocks starten, solltest sie aber jeweils in eigenen Kind-Coroutines sammeln, wenn sie parallel laufen sollen:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.uiState.collect { render(it) }
}
launch {
viewModel.effect.collect { effect -> handleEffect(effect) }
}
}
}
Ohne die inneren launch-Aufrufe würde die erste collect oft dauerhaft laufen und die zweite Zeile nie erreichen. Das ist ein klassischer Flow-Fehler: collect ist suspendierend. Wenn ein Flow nicht endet, blockiert er den restlichen Code in derselben Coroutine. Lifecycle-aware Collection ändert daran nichts. Sie gibt dir den richtigen Rahmen, aber du musst Coroutines weiterhin sauber strukturieren.
Für Tests und Code-Reviews kannst du konkrete Fragen nutzen. Wird UI-State in Compose mit einer lifecycle-aware State-API gesammelt? Nutzt ein Fragment den viewLifecycleOwner? Gibt es manuelle collect-Aufrufe in onCreate, onStart oder onViewCreated, die dauerhaft laufen könnten? Werden einzelne Events mehrfach ausgelöst, wenn du den Screen verlässt und zurückkehrst? Solche Fragen finden viele Lifecycle-Probleme, bevor sie als Fehlerberichte auftauchen.
Du kannst dein Verständnis praktisch prüfen, indem du in einem Beispielprojekt Logs in den Flow und in den Collector setzt. Öffne den Screen, drücke Home, kehre zurück und beobachte, wann gesammelt wird. Setze außerdem Breakpoints in render oder im Compose-Screen und prüfe, ob Aktualisierungen nur stattfinden, wenn der Screen sichtbar ist. Für ViewModels kannst du separat testen, ob uiState die richtigen Werte liefert. Für die UI prüfst du, ob die Collection an den richtigen Lifecycle gebunden ist. Diese Trennung macht die Fehlersuche deutlich klarer.
Fazit
Lifecycle-Aware Flow Collection ist keine Zusatzverzierung, sondern eine Grundregel für sichere Android-UIs mit Kotlin Flow. Du sammelst Datenströme nur dann, wenn der Screen sie verarbeiten kann, und lässt Android das Starten und Stoppen passend zum Lifecycle steuern. Nutze repeatOnLifecycle in Fragmenten und Activities, achte bei Fragmenten auf viewLifecycleOwner, und verwende in Compose lifecycle-aware State-Collection. Prüfe deinen nächsten Screen bewusst: Wo wird gesammelt, welcher Lifecycle begrenzt die Collection, und was passiert beim Verlassen und Zurückkehren? Genau diese Fragen solltest du im Debugger, in Tests und im Code-Review stellen.