SharedFlow
SharedFlow verteilt Ereignisse an mehrere Sammler. Du lernst Replay, Broadcasts und typische Fallen.
SharedFlow hilft dir, Ereignisse in einer Android-App sauber zu verteilen: ein ViewModel sendet etwas, und eine oder mehrere Stellen können darauf reagieren. Das klingt zuerst ähnlich wie StateFlow, LiveData oder ein Callback, hat aber einen anderen Schwerpunkt: SharedFlow ist besonders nützlich für eventartige Daten wie Navigation, Snackbars, Toasts, einmalige Fehlermeldungen oder interne Broadcasts zwischen Schichten.
Was ist das?
Ein SharedFlow ist ein Flow aus Kotlin Coroutines, der Werte an mehrere Sammler senden kann. Er ist ein sogenannter Hot Flow: Er existiert unabhängig davon, ob gerade jemand sammelt. Wenn dein ViewModel ein Ereignis auslöst, kann dieses Ereignis an alle aktiven Collector weitergegeben werden. Das passt zu Situationen, in denen nicht ein dauerhafter Zustand beschrieben wird, sondern etwas passiert.
Der wichtigste Unterschied zum normalen Flow ist das mentale Modell. Ein normaler Flow startet seine Arbeit oft neu, sobald du ihn sammelst. Ein SharedFlow dagegen ist eher wie ein gemeinsamer Kanal innerhalb deiner App-Schicht. Er wartet nicht darauf, dass ein einzelner Collector kommt, sondern kann Ereignisse produzieren, während Collector kommen und gehen.
Im Android-Kontext ist das relevant, weil UI-Lebenszyklen ständig wechseln. Eine Activity wird pausiert, eine Compose-Funktion wird neu zusammengesetzt, ein Screen wird rotiert, ein Nutzer geht zurück und wieder vor. Wenn du Ereignisse falsch modellierst, entstehen typische Fehler: eine Snackbar erscheint doppelt, eine Navigation wird nach Rotation erneut ausgeführt, oder ein Fehler verschwindet, weil gerade noch kein Collector aktiv war.
SharedFlow löst nicht automatisch alle diese Probleme. Er gibt dir aber die passenden Werkzeuge: Broadcast an mehrere Sammler, optionales Replay älterer Werte und Pufferregeln für kurze Phasen ohne aktiven Empfänger. Genau deshalb solltest du SharedFlow bewusst einsetzen und nicht als Standardersatz für jeden UI-State verwenden.
Für stabilen UI-Zustand ist StateFlow meist passender. Ein Ladezustand, ein Formularwert oder eine Liste von Einträgen sollen jederzeit den aktuellen Stand zeigen. Ein Klick-Event, eine Navigationsanweisung oder eine einmalige Erfolgsmeldung sind dagegen Ereignisse. Diese Trennung ist in realen Android-Projekten wichtig, weil sie Code-Reviews, Tests und spätere Erweiterungen deutlich einfacher macht.
Wie funktioniert es?
In der Praxis arbeitest du meistens mit MutableSharedFlow im ViewModel und gibst nach außen nur SharedFlow frei. So bleibt das Senden von Ereignissen gekapselt, während die UI nur sammeln kann. Das entspricht dem üblichen Android-Architekturgedanken: Das ViewModel verwaltet Logik, die UI beobachtet und reagiert.
Ein SharedFlow hat drei Eigenschaften, die du verstehen solltest: Replay, Puffer und Auslieferung an mehrere Collector.
Replay legt fest, wie viele bereits gesendete Werte ein neuer Collector sofort erhält. Bei replay = 0 bekommt ein neuer Collector keine alten Ereignisse. Das ist für viele UI-Ereignisse sinnvoll, weil eine alte Navigation oder Snackbar nicht erneut ausgelöst werden soll. Bei replay = 1 erhält ein neuer Collector den letzten Wert. Das kann für eventartige Broadcasts nützlich sein, ist aber bei einmaligen UI-Aktionen riskant.
Der Puffer entscheidet, was passiert, wenn gesendet wird, aber ein Collector gerade nicht schnell genug ist. extraBufferCapacity kann kurze Spitzen abfangen. Außerdem gibt es Strategien für Überlauf, etwa ob alte oder neue Werte verworfen werden. Für Einsteiger ist wichtig: Puffer sind kein Ersatz für sauberes Zustandsdesign. Wenn ein Ereignis fachlich nicht verloren gehen darf, solltest du prüfen, ob es wirklich ein Ereignis ist oder als Zustand modelliert werden muss.
Die Broadcast-Eigenschaft bedeutet: Mehrere aktive Collector können dasselbe Ereignis erhalten. Das ist praktisch, wenn zum Beispiel Logging, UI und ein Debug-Panel auf denselben internen Stream reagieren. In einer typischen App sammelst du UI-Events aber oft nur an einer Stelle, etwa im Screen. Gerade dann solltest du vermeiden, denselben SharedFlow an mehreren UI-Orten parallel zu sammeln, wenn dadurch dieselbe Aktion doppelt ausgeführt werden kann.
Ein häufiger Denkfehler ist die Idee eines „One-Time-Events“ als Wert, der in einem StateFlow liegt und nach dem Konsum gelöscht wird. Das führt oft zu kompliziertem Code mit Wrappern, consumed-Flags oder Rennen zwischen UI und ViewModel. SharedFlow ist für viele dieser Fälle besser geeignet, weil das Ereignis als Ereignis gesendet wird. Trotzdem musst du den Lebenszyklus korrekt behandeln.
In Compose sammelst du SharedFlow nicht direkt im Composable-Body. Der Body kann häufig neu ausgeführt werden. Stattdessen verwendest du einen Effekt wie LaunchedEffect, damit das Sammeln an eine Coroutine gebunden ist. In klassischen Views verwendest du repeatOnLifecycle, damit nur in einem passenden Lifecycle-Zustand gesammelt wird. So verhinderst du, dass Collector unkontrolliert weiterlaufen oder mehrfach registriert werden.
In der Praxis
Stell dir einen Login-Screen vor. Der sichtbare Zustand enthält Felder wie isLoading, emailError und vielleicht canSubmit. Nach einem erfolgreichen Login soll die App aber nur einmal zur Startseite navigieren. Diese Navigation ist kein dauerhafter Zustand des Login-Formulars, sondern ein Ereignis. Dafür kannst du einen SharedFlow verwenden.
sealed interface LoginEvent {
data object NavigateHome : LoginEvent
data class ShowMessage(val text: String) : LoginEvent
}
class LoginViewModel(
private val repository: AuthRepository
) : ViewModel() {
private val _events = MutableSharedFlow<LoginEvent>(
replay = 0,
extraBufferCapacity = 1
)
val events: SharedFlow<LoginEvent> = _events
fun submit(email: String, password: String) {
viewModelScope.launch {
val result = repository.login(email, password)
if (result.isSuccess) {
_events.emit(LoginEvent.NavigateHome)
} else {
_events.emit(
LoginEvent.ShowMessage("Anmeldung fehlgeschlagen")
)
}
}
}
}
@Composable
fun LoginScreen(
viewModel: LoginViewModel,
onNavigateHome: () -> Unit,
snackbarHostState: SnackbarHostState
) {
LaunchedEffect(viewModel) {
viewModel.events.collect { event ->
when (event) {
LoginEvent.NavigateHome -> onNavigateHome()
is LoginEvent.ShowMessage -> {
snackbarHostState.showSnackbar(event.text)
}
}
}
}
// UI-State und Eingabefelder würden hier separat dargestellt.
}
Das Beispiel zeigt die Trennung zwischen Zustand und Ereignis. Die UI kann weiterhin ihren normalen State beobachten, zum Beispiel über StateFlow und collectAsStateWithLifecycle, während Ereignisse über SharedFlow laufen. Navigation und Snackbar werden nicht in einem State-Objekt „geparkt“, sondern beim Auftreten behandelt.
Die Entscheidungsregel lautet: Nutze StateFlow für Daten, die die UI jederzeit korrekt rekonstruieren muss. Nutze SharedFlow für Ereignisse, die während der aktiven Nutzung passieren und nicht automatisch den aktuellen UI-Zustand beschreiben. Wenn ein neuer Screen nach einer Rotation den letzten Wert erneut sehen muss, ist das eher Zustand. Wenn eine Aktion nur auf einen Moment reagieren soll, ist SharedFlow ein Kandidat.
Achte besonders auf replay. Für Navigation, Snackbar und Dialoge ist replay = 0 meistens die sicherste Voreinstellung. Mit replay = 1 kann ein neuer Collector ein altes Ereignis erneut erhalten. Das passiert zum Beispiel, wenn ein Composable aus dem Navigations-Backstack neu aufgebaut wird. Dann kann die App wieder navigieren oder dieselbe Meldung erneut anzeigen. Dieser Fehler wirkt oft zufällig, weil er nur bei Lifecycle-Wechseln, Rotation oder schneller Navigation auffällt.
Eine weitere Stolperfalle ist mehrfaches Sammeln. Wenn du denselben SharedFlow in zwei Composables sammelst, reagieren beide. Für reine Analyse-Events kann das gewollt sein. Für UI-Aktionen ist es oft ein Bug. Sammle solche Events an einer klaren Stelle, meist am Screen-Rand, nicht tief in wiederverwendbaren UI-Komponenten. Komponenten sollten lieber Callbacks anbieten, während der Screen entscheidet, wie er mit Ereignissen umgeht.
Auch Tests werden durch diese Trennung klarer. Du kannst im ViewModel-Test eine Coroutine starten, den events-Flow sammeln, submit() ausführen und prüfen, ob NavigateHome oder ShowMessage gesendet wurde. Wichtig ist dabei, den Test nicht nur auf den aktuellen UI-State zu beschränken. Ereignisse sind ein eigener Ausgabekanal deiner ViewModel-Logik und sollten bei kritischen Abläufen ebenfalls geprüft werden.
Beim Debuggen hilft es, den Lebenszyklus bewusst zu provozieren. Starte die App, löse ein Ereignis aus, rotiere das Gerät oder gehe kurz in den Hintergrund. Wenn eine Snackbar doppelt erscheint oder Navigation erneut ausgeführt wird, prüfe zuerst replay, die Anzahl deiner Collector und den Ort, an dem du sammelst. In Code-Reviews solltest du besonders misstrauisch werden, wenn Event-Wrapper in State-Klassen auftauchen oder wenn ein SharedFlow ohne klare Begründung Replay aktiviert.
SharedFlow ist außerdem kein Ersatz für persistente Ereignisse. Wenn eine Bestellung abgeschickt wurde, eine Zahlung bestätigt ist oder ein kritischer Fehler später noch sichtbar sein muss, gehört diese Information in einen dauerhaften Zustand oder in eine Datenquelle. SharedFlow ist für die Verteilung während der Laufzeit gedacht. Für fachlich wichtige Daten brauchst du ein Modell, das auch Prozess-Tod, Neustart oder Offline-Situationen übersteht.
Fazit
SharedFlow ist ein präzises Werkzeug für eventartige Streams in modernen Android-Apps: Du sendest Ereignisse aus dem ViewModel, sammelst sie lifecycle-bewusst in der UI und steuerst mit Replay, ob neue Collector alte Werte erhalten. Prüfe dein Verständnis aktiv, indem du ein kleines Login- oder Formularbeispiel baust, Rotation testest, doppelte Collector suchst und in einem ViewModel-Test erwartete Events sammelst. Wenn du bei jedem Flow bewusst entscheidest, ob er Zustand oder Ereignis beschreibt, vermeidest du viele typische Fehler rund um Navigation, Snackbars und einmalige UI-Reaktionen.