Hot Flow in Android mit StateFlow und SharedFlow
Hot Flows laufen unabhängig vom einzelnen Collector. Du lernst, wann StateFlow, SharedFlow und Sharing in Android passen.
Ein Hot Flow ist ein Datenstrom, der nicht erst durch einen einzelnen Collector entsteht. Für Android ist das wichtig, weil UI, ViewModel, Repository und Hintergrundarbeit oft denselben Zustand oder dieselben Ereignisse beobachten müssen, ohne dass jede Beobachtung die ganze Arbeit neu startet.
Was ist das?
Ein Flow beschreibt in Kotlin einen Strom von Werten über die Zeit. Bei einem Cold Flow beginnt die Arbeit typischerweise erst, wenn ein Collector collect aufruft. Ein Hot Flow läuft dagegen als eigenständige Quelle: Er kann Werte halten, senden oder weiterverteilen, auch wenn gerade kein Collector aktiv ist. Der einzelne Collector ist also nicht der Besitzer der Arbeit, sondern nur ein Beobachter.
In Android triffst du Hot Flows besonders im ViewModel. Dort soll UI-Zustand stabil verfügbar sein, auch wenn Compose wegen Recomposition neu liest oder ein Screen nach einer Konfigurationsänderung wieder erscheint. Genau dafür ist StateFlow gedacht: Er hat immer einen aktuellen Wert und sendet diesen Wert sofort an neue Collector. SharedFlow ist allgemeiner. Er verteilt Werte an mehrere Collector und kann je nach Konfiguration alte Werte erneut ausliefern oder nur neue Werte senden.
Das mentale Modell ist: Ein Hot Flow ist wie eine laufende Ansage in deiner App-Architektur. Wer zuhört, bekommt Werte. Wer später dazukommt, bekommt je nach Typ und Einstellung den letzten bekannten Zustand, mehrere gespeicherte Werte oder nur künftige Werte. Damit löst du ein reales Problem: Du trennst die Erzeugung von Daten von der Frage, welche UI gerade sichtbar ist.
Wie funktioniert es?
StateFlow ist ein Hot Flow für Zustand. Er braucht einen Startwert, hält immer genau einen aktuellen Wert und sendet nur dann weiter, wenn sich der Wert ändert. Im ViewModel verwendest du häufig einen privaten MutableStateFlow und veröffentlichst nur die unveränderliche Sicht als StateFlow. So bleibt die Kontrolle über Zustandsänderungen im ViewModel.
SharedFlow ist flexibler. Er braucht keinen aktuellen Zustand. Mit replay kannst du festlegen, wie viele zuletzt gesendete Werte neue Collector nachträglich erhalten. Mit extraBufferCapacity und dem Overflow-Verhalten steuerst du, was passiert, wenn schneller gesendet als gesammelt wird. Das passt zu Ereignissen, Broadcast-artigen Signalen oder Daten, die mehrere Komponenten parallel beobachten sollen.
Der Begriff sharing wird wichtig, wenn du einen Cold Flow in einen Hot Flow verwandelst. Mit Operatoren wie stateIn oder shareIn bindest du den Flow an einen CoroutineScope, oft viewModelScope. Außerdem entscheidest du mit einer Sharing-Strategie, wann der upstream Flow gestartet und gestoppt wird. In Android ist SharingStarted.WhileSubscribed(...) häufig sinnvoll, weil die teure Arbeit nur aktiv bleibt, solange es Beobachter gibt, mit einer kurzen Nachlaufzeit gegen unnötiges Stoppen und Neustarten.
In Compose liest du einen StateFlow nicht direkt mit endlosen manuellen Collectors in Composables. Üblich ist, den Flow lifecycle-bewusst als State zu sammeln, damit die UI nur aktiv beobachtet, wenn sie sich in einem passenden Lifecycle-Zustand befindet. Dadurch vermeidest du unnötige Arbeit, doppelte Collector und Updates, die an eine nicht sichtbare Oberfläche gehen.
In der Praxis
Ein typisches Muster ist ein ViewModel, das einen Repository-Flow in UI-State umwandelt und als StateFlow anbietet. Das Repository kann intern aus Datenbank, Netzwerk oder Cache lesen. Das ViewModel entscheidet, wie dieser Strom für die UI stabil gehalten wird.
data class ArticleUiState(
val isLoading: Boolean = true,
val titles: List<String> = emptyList(),
val errorMessage: String? = null
)
class ArticleViewModel(
repository: ArticleRepository
) : ViewModel() {
val uiState: StateFlow<ArticleUiState> =
repository.observeArticles()
.map { articles ->
ArticleUiState(
isLoading = false,
titles = articles.map { it.title }
)
}
.catch { throwable ->
emit(
ArticleUiState(
isLoading = false,
errorMessage = throwable.message ?: "Unbekannter Fehler"
)
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = ArticleUiState()
)
}
Der wichtige Punkt ist nicht die Länge des Codes, sondern die Besitzfrage. Das ViewModel besitzt den UI-State. Die UI sammelt ihn nur. Wenn ein Composable verschwindet und später wieder erscheint, muss der Zustand nicht neu erfunden werden. Neue Collector erhalten direkt den letzten Wert aus dem StateFlow.
Für einmalige Signale ist StateFlow dagegen oft die falsche Wahl. Eine Snackbar-Nachricht oder ein Navigationsereignis ist kein dauerhafter Zustand, der immer wieder angezeigt werden soll. Dafür kann ein MutableSharedFlow passen:
class LoginViewModel : ViewModel() {
private val _events = MutableSharedFlow<LoginEvent>()
val events: SharedFlow<LoginEvent> = _events
fun onLoginFailed() {
viewModelScope.launch {
_events.emit(LoginEvent.ShowMessage("Anmeldung fehlgeschlagen"))
}
}
}
sealed interface LoginEvent {
data class ShowMessage(val text: String) : LoginEvent
}
Eine häufige Stolperfalle ist, jeden Flow vorschnell hot zu machen. Ein Hot Flow lebt in einem Scope und kann Arbeit aktiv halten. Wenn du shareIn oder stateIn mit einem zu langen Scope und einer aggressiven Startstrategie verwendest, laufen Datenbankabfragen, Netzwerk-Polling oder Sensorzugriffe vielleicht weiter, obwohl kein Screen sie braucht. Deine Entscheidungsregel kann deshalb lauten: Nutze StateFlow für aktuellen UI-Zustand, SharedFlow für geteilte Ereignisse oder Signale, und wähle sharing so, dass teure Arbeit an echte Beobachtung gebunden bleibt.
Prüfe außerdem in Code-Reviews, ob ein öffentliches MutableStateFlow oder MutableSharedFlow sichtbar ist. Das ist meistens ein Designfehler, weil dann jede Klasse Werte ändern kann. Besser ist ein privates Mutable-Objekt und ein öffentliches read-only Interface. In Tests kannst du Hot Flows gut validieren, indem du den Startwert, spätere Emissionen und das Verhalten neuer Collector prüfst. Beim Debuggen hilft es, Log-Ausgaben an der Quelle und am Collector zu setzen, damit du siehst, ob der upstream Flow mehrfach gestartet wird.
Fazit
Hot Flows helfen dir, Android-Datenströme sauber zu modellieren, wenn Werte unabhängig von einem einzelnen Collector existieren sollen. Baue dir als Übung ein kleines ViewModel mit StateFlow für Screen-State und SharedFlow für UI-Ereignisse, beobachte es in Compose, und prüfe im Debugger oder Test, wann Collector starten, welche Werte neue Collector erhalten und ob teure Arbeit wirklich nur so lange läuft, wie deine App sie braucht.