SharedFlow ist ein heißer reaktiver Flow aus der Kotlin Coroutines Bibliothek, optimiert für einmalige Ereignisse (one-shot events), die sich bei Bildschirmdrehung oder Neuerstellung des Abonnenten nicht wiederholen sollen. Wir zeigen, wie sich SharedFlow von StateFlow unterscheidet: Im Gegensatz zu StateFlow speichert SharedFlow den letzten Wert nicht für neue Abonnenten und unterstützt die Konfiguration von replay, extraBufferCapacity und onBufferOverflow. Laut Google (Android Developers, 2025) ist SharedFlow die empfohlene Lösung für Navigationsbefehle, Snackbar-Nachrichten und andere Ereignisse, die genau einmal verarbeitet werden sollen.
Wichtige Punkte
SharedFlow ist ein heißer Flow (hot flow) aus der Bibliothek kotlinx.coroutines.flow, der im Gegensatz zu StateFlow nicht an einen einzelnen Zustand gebunden ist und eine beliebige Anzahl von Ereignissen an beliebige Abonnenten senden kann. SharedFlow ist der Basistyp für StateFlow — StateFlow wird tatsächlich über SharedFlow mit replay = 1 implementiert.
Das Hauptmerkmal von SharedFlow ist, dass es den letzten Wert nicht speichern muss. Standardmäßig (replay = 0) erhält ein neuer Abonnent nichts, bis ein neues Ereignis gesendet wird. Dies macht SharedFlow ideal für Szenarien, in denen ein Ereignis genau einmal verarbeitet werden soll: Navigation, Snackbar, Systembenachrichtigungen, QR-Code-Scanergebnisse.
SharedFlow wurde in kotlinx.coroutines 1.4.0 (November 2020) zusammen mit StateFlow stabilisiert. Laut der Kotlin Coroutines Dokumentation (2025) verwendet SharedFlow feinkörnige Sperren für die Synchronisierung von Abonnenten und bietet lineare Skalierbarkeit auf über 1000 gleichzeitige Abonnenten ohne Leistungseinbußen, bestätigt durch JetBrains-Tests.
Die Wahl zwischen SharedFlow und StateFlow hängt von der Semantik der übertragenen Daten ab: Zustand (StateFlow) oder Ereignis (SharedFlow). Nachfolgend finden Sie klare Kriterien mit Beispielen.
| Kriterium | SharedFlow | StateFlow |
|---|---|---|
| Semantik | Einmalige Ereignisse (Navigation, Toast, Alert) | UI-Zustand (Liste, Laden, Fehler) |
| Anfangswert | Nicht erforderlich | Erforderlich |
| Wiederholung bei Abonnement | Nur wenn replay > 0 | Immer letzter Wert |
| Konflation | Nein — Ereignisse gehen nicht verloren (wenn Puffer nicht voll) | Ja — speichert nur den neuesten |
| Pufferung | Konfigurierbar über replay + extraBufferCapacity | Nur 1 (replay=1 fest) |
| Verwendung | navigationEvent, showSnackbar, openDialog | items, isLoading, uiState |
Die einfachste Regel: Wenn die Daten bei Bildschirmdrehung angezeigt werden sollen — ist es Zustand (StateFlow). Wenn sich das Ereignis bei Bildschirmdrehung nicht wiederholen soll — ist es ein einmaliges Ereignis (SharedFlow). Zum Beispiel ein Toast mit einer Fehlermeldung — SharedFlow: bei Drehung soll der Toast nicht erneut erscheinen. Eine Produktliste — StateFlow: bei Drehung soll die Liste auf dem Bildschirm bleiben.
Bei IT Sectr verwenden wir SharedFlow für: Navigationsbefehle (Bildschirmübergang, Deep-Link-Öffnung), UI-Ereignisse (Snackbar, AlertDialog), Systembenachrichtigungen (Hintergrunddatenaktualisierungen, Zahlungsergebnis), Analyseereignisse (Protokollierung, Tracking).
MutableSharedFlow ist die veränderbare Version von SharedFlow mit den Methoden emit() (suspend) und tryEmit() (nicht suspend) zum Senden von Ereignissen. emit() wird angehalten, wenn der Puffer voll ist und onBufferOverflow = SUSPEND. tryEmit() gibt einen Boolean zurück, der angibt, ob das Ereignis erfolgreich zum Puffer hinzugefügt wurde.
class EventBus {
private val _events = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 10,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<UiEvent> get() = _events
suspend fun sendEvent(event: UiEvent) {
_events.emit(event)
}
fun trySendEvent(event: UiEvent): Boolean {
return _events.tryEmit(event)
}
}
sealed interface UiEvent {
data class ShowSnackbar(val message: String) : UiEvent
data class NavigateTo(val route: String) : UiEvent
data class ShowDialog(val title: String, val message: String) : UiEvent
}
Die Konstruktorparameter sind von entscheidender Bedeutung: replay = 0 stellt sicher, dass das Ereignis für einen neuen Abonnenten nicht wiederholt wird; extraBufferCapacity = 10 bietet einen Puffer für schnelles Senden von Ereignissen, bevor die UI abonniert; DROP_OLDEST ist die Überlaufstrategie: alte Ereignisse werden verworfen, neue werden beibehalten. Laut Kotlin Coroutines Performance (JetBrains, 2024) verarbeitet SharedFlow mit extraBufferCapacity = 64 über 100.000 Ereignisse pro Sekunde ohne Verluste.
Event-Muster (oder UiEvent) ist die von Google empfohlene Methode, um einmalige Ereignisse von ViewModel an View zu übergeben. Im Gegensatz zum Zustand (StateFlow) sollte ein Ereignis genau einmal verarbeitet werden und bei Bildschirmdrehung nicht wiederholt werden. SharedFlow mit replay = 0 ist ideal für diese Aufgabe.
class CheckoutViewModel : ViewModel() {
private val _uiState = MutableStateFlow<CheckoutState>(CheckoutState.Idle)
val uiState: StateFlow<CheckoutState> get() = _uiState
private val _event = MutableSharedFlow<CheckoutEvent>()
val event: SharedFlow<CheckoutEvent> get() = _event
fun placeOrder() {
viewModelScope.launch {
_uiState.value = CheckoutState.Loading
try {
val orderId = orderRepository.createOrder(cart)
_uiState.value = CheckoutState.Success(orderId)
_event.emit(CheckoutEvent.NavigateToOrderTracking(orderId))
} catch (e: Exception) {
_uiState.value = CheckoutState.Error(e.message)
_event.emit(CheckoutEvent.ShowErrorSnackbar(e.message ?: "Layoutfehler"))
}
}
}
}
sealed interface CheckoutEvent {
data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}
In der View (Activity/Fragment): Das Abonnement von Ereignissen sollte in lifecycleScope mit repeatOnLifecycle(STATE.STARTED) erfolgen. Bei jedem Eintritt in STARTED wird das Abonnement neu erstellt, aber das Ereignis wiederholt sich nicht, da SharedFlow mit replay=0 es bereits freigegeben hat. Dadurch wird sichergestellt, dass die Navigation zum Bestellverfolgungsbildschirm nur einmal erfolgt, nicht bei jeder Drehung.
Der Konstruktor von MutableSharedFlow akzeptiert drei Parameter, die das Pufferverhalten bestimmen. Eine falsche Konfiguration kann zu Ereignisverlust oder Blockierung von emit() führen.
| Parameter | Typ | Standard | Beschreibung |
|---|---|---|---|
| replay | Int | 0 | Anzahl der letzten Ereignisse, die einem neuen Abonnenten wiedergegeben werden. 0 = nicht wiedergeben, 1 = wie StateFlow |
| extraBufferCapacity | Int | 0 | Zusätzlicher Puffer über replay hinaus. Ereignisse werden in einem ringförmigen Puffer gespeichert. 64 ist die empfohlene Grenze für die meisten Szenarien |
| onBufferOverflow | BufferOverflow | SUSPEND | Strategie bei vollem Puffer: SUSPEND, DROP_OLDEST, DROP_LATEST |
// Konfigurationen für verschiedene Szenarien:
// 1. Einmalige UI-Ereignisse (Navigation, Toasts)
val uiEvents = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 5,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
// 2. Replay-Flow zur Zustandssynchronisation (wie StateFlow)
val stateLike = MutableSharedFlow<AppState>(
replay = 1,
extraBufferCapacity = 0
)
// 3. Hochfrequentes Senden von Ereignissen (Analytics, Logs)
val analytics = MutableSharedFlow<AnalyticsEvent>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
Wichtig: extraBufferCapacity + replay = Gesamtpuffergröße. Wenn emit() schneller aufgerufen wird, als der Abonnent Ereignisse verarbeitet, füllt sich der Puffer und onBufferOverflow wird ausgelöst. Für UI-Ereignisse ist DROP_OLDEST eine sichere Strategie: alte Ereignisse (nicht mehr relevante Navigationen) werden zugunsten neuer verworfen. Für Finanztransaktionen verwenden Sie SUSPEND — dies garantiert, dass kein Ereignis verloren geht, auf Kosten der Blockierung des Senders.
Navigationsbefehle sind ein klassischer Anwendungsfall für SharedFlow. Ein Fragment abonniert Ereignisse und führt die Navigation durch. Bei Bildschirmdrehung wiederholt sich der Befehl nicht.
// ViewModel
class AuthViewModel : ViewModel() {
private val _navEvent = MutableSharedFlow<NavEvent>()
val navEvent: SharedFlow<NavEvent> get() = _navEvent
fun onLoginSuccess() {
viewModelScope.launch {
_navEvent.emit(NavEvent.NavigateTo(NavRoutes.HOME))
}
}
fun onLogout() {
viewModelScope.launch {
_navEvent.emit(NavEvent.NavigateTo(NavRoutes.LOGIN))
}
}
}
sealed interface NavEvent {
data class NavigateTo(val route: String) : NavEvent
data class NavigateBack(val popUpTo: String? = null) : NavEvent
}
// Im Fragment:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.navEvent.collect { navEvent ->
when (navEvent) {
is NavEvent.NavigateTo -> findNavController().navigate(navEvent.route)
is NavEvent.NavigateBack -> findNavController().popBackStack()
}
}
}
}
Ein komplexes Szenario: SharedFlow für Hintergrundereignisbenachrichtigungen kombiniert mit StateFlow für die UI.
class NotificationViewModel : ViewModel() {
private val _toastMessage = MutableSharedFlow<String>()
val toastMessage: SharedFlow<String> get() = _toastMessage
private val _notifications = MutableStateFlow<List<Notification>>(emptyList())
val notifications: StateFlow<List<Notification>> get() = _notifications
init {
viewModelScope.launch {
notificationChannel
.consumeAsFlow()
.collect { notification ->
_notifications.value = _notifications.value + notification
_toastMessage.emit("Neue Benachrichtigung: ${notification.title}")
}
}
}
fun dismissNotification(id: String) {
_notifications.value = _notifications.value.filter { it.id != id }
}
fun markAllRead() {
viewModelScope.launch {
_notifications.value = _notifications.value.map { it.copy(isRead = true) }
_toastMessage.emit("Alle Benachrichtigungen als gelesen markiert")
}
}
}
In diesem Beispiel: StateFlow speichert die Benachrichtigungsliste (Zustand — bleibt bei Drehung erhalten), SharedFlow sendet Toast-Nachrichten (einmalige Ereignisse — werden bei Drehung nicht wiederholt). Die Kombination zweier Flow-Typen ist das von Google ab 2022 empfohlene Muster für ViewModel.
Häufig gestellte Fragen
Ja, wenn der Puffer voll ist und onBufferOverflow = DROP_OLDEST oder DROP_LATEST. SharedFlow garantiert nicht die Zustellung jedes Ereignisses — es ist keine Nachrichtenwarteschlange (wie Channel). Wenn Sie eine garantierte Zustellung aller Ereignisse benötigen, verwenden Sie Channel mit unbegrenztem Puffer (UNLIMITED) oder BroadcastChannel (veraltet). Bei UI-Ereignissen ist der Verlust veralteter Ereignisse (z. B. alte Navigation) erwartetes Verhalten, kein Fehler.
Channel ist eine FIFO-Warteschlange, bei der jedes Ereignis genau einem Abonnenten zugestellt wird (Punkt-zu-Punkt). SharedFlow ist eine Übertragung: jedes Ereignis wird ALLEN aktiven Abonnenten zugestellt. SharedFlow ist näher an BroadcastChannel (der veraltet ist) und eignet sich für Eins-zu-Viele-Szenarien. Channel ist für Eins-zu-Eins (Thread-Pools, Pipelines). Laut JetBrains-Empfehlung ist SharedFlow der Ersatz für BroadcastChannel in allen neuen Projekten.
SharedFlow ist bereits threadsicher — emit() und collect() sind ordnungsgemäß synchronisiert. Mehrere Threads können emit() ohne Sperren aufrufen, und alle aktiven Abonnenten erhalten Ereignisse in der richtigen Reihenfolge. tryEmit() ist nicht blockierend — es gibt false zurück, wenn der Puffer voll ist. Für hochbelastete Systeme verwenden Sie tryEmit() mit DROP_OLDEST — dies verhindert Thread-Blockierung.
SharedFlow ohne replay=1 speichert den letzten Wert nicht — bei Bildschirmdrehung erhält ein neuer Abonnent den aktuellen Zustand nicht und die UI bleibt leer. Mit replay=1 verhält sich SharedFlow wie StateFlow, verliert jedoch die Optimierung des Vergleichs durch equals(), was bei erneuter Sendung desselben Werts unnötige Benachrichtigungen verursacht. StateFlow ist die richtige Wahl für Zustand; SharedFlow ist für Ereignisse.
Zum Testen von SharedFlow verwenden Sie Turbine — eine Kotlin-Bibliothek zum Testen von Flow. Turbine ermöglicht die Überprüfung jeder einzelnen Ausgabe mit Timeouts und Abschlussbestätigung. Beispiel: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. Sie können auch .toList() in runTest mit Angabe der erwarteten Ereignisanzahl verwenden.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch