SharedFlow — een hete reactieve stroom uit de Kotlin Coroutines-bibliotheek, geoptimaliseerd voor eenmalige gebeurtenissen (one-shot events) die niet mogen worden herhaald bij het draaien van het scherm of het opnieuw aanmaken van een abonnee. We laten zien hoe SharedFlow verschilt van StateFlow: in tegenstelling tot StateFlow slaat SharedFlow de laatste waarde niet op voor nieuwe abonnees en ondersteunt het configuratie van replay, extraBufferCapacity en onBufferOverflow. Volgens Google (Android Developers, 2025) is SharedFlow de aanbevolen oplossing voor navigatieopdrachten, Snackbar-berichten en andere gebeurtenissen die precies één keer moeten worden verwerkt.
Belangrijkste
SharedFlow — is een hete stroom (hot flow) uit de kotlinx.coroutines.flow-bibliotheek die, in tegenstelling tot StateFlow, niet aan één enkele toestand is gebonden en een willekeurig aantal gebeurtenissen naar willekeurige abonnees kan emitteren. SharedFlow is het basistype voor StateFlow — StateFlow is geïmplementeerd via SharedFlow met replay = 1.
Kernkenmerk van SharedFlow — het hoeft de laatste waarde niet op te slaan. Standaard (replay = 0) ontvangt een nieuwe abonnee niets totdat er een nieuwe gebeurtenis wordt verzonden. Dit maakt SharedFlow ideaal voor scenario's waarin een gebeurtenis precies één keer moet worden verwerkt: navigatie, Snackbar, systeemmeldingen, QR-code scanresultaten.
SharedFlow is gestabiliseerd in kotlinx.coroutines 1.4.0 (november 2020) samen met StateFlow. Volgens de Kotlin Coroutines-documentatie (2025) gebruikt SharedFlow fijnmazige vergrendeling voor synchronisatie van abonnees en biedt het lineaire schaalbaarheid tot 1000+ gelijktijdige abonnees zonder prestatievermindering, bevestigd door JetBrains-tests.
De keuze tussen SharedFlow en StateFlow hangt af van de semantiek van de verzonden gegevens: toestand (StateFlow) of gebeurtenis (SharedFlow). Hieronder duidelijke criteria met voorbeelden.
| Criterium | SharedFlow | StateFlow |
|---|---|---|
| Semantiek | Eenmalige gebeurtenissen (navigatie, toast, alert) | UI-toestand (lijst, laden, fout) |
| Beginwaarde | Niet vereist | Verplicht |
| Herhaling bij abonnement | Alleen als replay > 0 | Altijd laatste waarde |
| Conflatie | Nee — gebeurtenissen gaan niet verloren (als buffer niet vol is) | Ja — slaat alleen laatste op |
| Buffering | Configureerbaar via replay + extraBufferCapacity | Alleen 1 (replay=1 vast) |
| Gebruik | navigationEvent, showSnackbar, openDialog | items, isLoading, uiState |
De eenvoudigste regel: als gegevens zichtbaar moeten zijn bij het draaien van het scherm — dan is het toestand (StateFlow). Als bij het draaien van het scherm de gebeurtenis niet mag worden herhaald — dan is het een eenmalige gebeurtenis (SharedFlow). Bijvoorbeeld „toast met foutmelding“ — SharedFlow: bij draaien mag de toast niet opnieuw verschijnen. „Productlijst“ — StateFlow: bij draaien moet de lijst op het scherm blijven.
Bij IT Sectr gebruiken we SharedFlow voor: navigatieopdrachten (overgang naar scherm, openen van diepe link), UI-gebeurtenissen (Snackbar, AlertDialog), systeemmeldingen (achtergrondgegevens bijwerken, betalingsresultaat), analytische gebeurtenissen (loggen, tracking).
MutableSharedFlow — wijzigbare versie van SharedFlow met methoden emit() (suspend) en tryEmit() (non-suspend) voor het verzenden van gebeurtenissen. emit() onderbreekt als de buffer vol is en onBufferOverflow = SUSPEND. tryEmit() retourneert Boolean — of de gebeurtenis succesvol aan de buffer is toegevoegd.
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
}
De constructorparameters zijn cruciaal: replay = 0 garandeert dat de gebeurtenis niet wordt herhaald voor een nieuwe abonnee; extraBufferCapacity = 10 — buffer voor snelle verzending van gebeurtenissen voordat de UI zich abonneert; DROP_OLDEST — strategie bij overloop: oude gebeurtenissen worden verwijderd, nieuwe worden bewaard. Volgens Kotlin Coroutines Performance (JetBrains, 2024) verwerkt SharedFlow met extraBufferCapacity = 64 meer dan 100.000 gebeurtenissen per seconde zonder verlies.
Het Event-patroon (of UiEvent) — de door Google aanbevolen manier om eenmalige gebeurtenissen van ViewModel naar View te sturen. In tegenstelling tot toestand (StateFlow) moet de gebeurtenis precies één keer worden verwerkt en bij het draaien van het scherm niet worden herhaald. SharedFlow met replay = 0 is hiervoor ideaal.
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 ?: "Opmaakfout"))
}
}
}
}
sealed interface CheckoutEvent {
data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}
In View (Activity/Fragment): abonnement op de gebeurtenis moet worden uitgevoerd in lifecycleScope met repeatOnLifecycle(STATE.STARTED). Bij elke toegang tot STARTED wordt het abonnement opnieuw aangemaakt, maar de gebeurtenis wordt niet herhaald omdat SharedFlow met replay=0 deze al heeft vrijgegeven. Dit garandeert dat navigatie naar het ordertrackingsscherm slechts één keer plaatsvindt, niet bij elke draaiing.
De constructor van MutableSharedFlow ontvangt drie parameters die het buffergedrag bepalen. Onjuiste configuratie kan leiden tot verlies van gebeurtenissen of blokkering van emit().
| Parameter | Type | Standaard | Beschrijving |
|---|---|---|---|
| replay | Int | 0 | Aantal laatste gebeurtenissen dat wordt afgespeeld voor een nieuwe abonnee. 0 = niet afspelen, 1 = als StateFlow |
| extraBufferCapacity | Int | 0 | Extra buffer bovenop replay. Gebeurtenissen worden opgeslagen in een ringbuffer. 64 — aanbevolen limiet voor de meeste scenario's |
| onBufferOverflow | BufferOverflow | SUSPEND | Strategie bij bufferoverloop: SUSPEND, DROP_OLDEST, DROP_LATEST |
// Configuraties voor verschillende scenario's:
// 1. Eenmalige UI-gebeurtenissen (navigatie, toasts)
val uiEvents = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 5,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
// 2. Replay-stroom voor toestandssynchronisatie (zoals StateFlow)
val stateLike = MutableSharedFlow<AppState>(
replay = 1,
extraBufferCapacity = 0
)
// 3. Hoogfrequente gebeurtenisverzending (analytics, logs)
val analytics = MutableSharedFlow<AnalyticsEvent>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
Belangrijk: extraBufferCapacity + replay = totale buffergrootte. Als emit() sneller wordt aangeroepen dan de abonnee de gebeurtenis verwerkt, raakt de buffer vol en wordt onBufferOverflow geactiveerd. Voor UI-gebeurtenissen is DROP_OLDEST een veilige strategie: oude gebeurtenissen (niet meer actuele navigaties) worden verwijderd ten gunste van nieuwe. Gebruik voor financiële transacties SUSPEND — dit garandeert dat geen enkele gebeurtenis verloren gaat ten koste van het blokkeren van de afzender.
Navigatieopdrachten — klassiek use case voor SharedFlow. Fragment abonneert zich op gebeurtenissen en voert navigatie uit. Bij het draaien van het scherm wordt de opdracht niet herhaald.
// 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
}
// In 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()
}
}
}
}
Complex scenario: SharedFlow voor meldingen over achtergrondgebeurtenissen gecombineerd met StateFlow voor 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("Nieuwe melding: ${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 meldingen gemarkeerd als gelezen")
}
}
}
In dit voorbeeld: StateFlow slaat de meldingenlijst op (toestand — blijft behouden bij draaien), SharedFlow emitteert toast-berichten (eenmalige gebeurtenissen — worden niet herhaald bij draaien). Combinatie van twee Flow-typen — het door Google aanbevolen patroon voor ViewModel sinds 2022.
Veelgestelde vragen
Ja, als de buffer vol is en onBufferOverflow = DROP_OLDEST of DROP_LATEST. SharedFlow garandeert niet de levering van elke gebeurtenis — dit is geen berichtenwachtrij (zoals Channel). Als gegarandeerde levering van alle gebeurtenissen nodig is, gebruik dan Channel met een niet-overlopen buffer (UNLIMITED) of BroadcastChannel (deprecated). Voor UI-gebeurtenissen is verlies van verouderde gebeurtenissen (bijv. oude navigatie) — verwacht gedrag, geen bug.
Channel — een FIFO-wachtrij waarin elke gebeurtenis aan precies één abonnee wordt geleverd (punt-tot-punt). SharedFlow — uitzending: elke gebeurtenis wordt aan ALLE actieve abonnees geleverd. SharedFlow lijkt meer op BroadcastChannel (dat deprecated is) en is geschikt voor „een-op-veel“-scenario's. Channel — voor „een-op-een“ (thread pools, pipeline). Volgens JetBrains-aanbeveling vervangt SharedFlow BroadcastChannel voor alle nieuwe projecten.
SharedFlow is al thread-safe — emit() en collect() zijn correct gesynchroniseerd. Meerdere threads kunnen emit() aanroepen zonder blokkeringen en alle actieve abonnees ontvangen gebeurtenissen in de juiste volgorde. tryEmit() is niet-blokkerend — retourneert false als de buffer vol is. Gebruik voor systemen met hoge belasting tryEmit() met DROP_OLDEST — dit voorkomt blokkering van threads.
SharedFlow zonder replay=1 slaat de laatste waarde niet op — bij het draaien van het scherm ontvangt de nieuwe abonnee de huidige toestand niet, blijft de UI leeg. Met replay=1 gedraagt SharedFlow zich als StateFlow, maar verliest het de optimalisatie van vergelijking via equals(), wat onnodige meldingen veroorzaakt bij het opnieuw verzenden van dezelfde waarde. StateFlow — de juiste keuze voor toestand; SharedFlow — voor gebeurtenissen.
Gebruik voor het testen van SharedFlow Turbine — de Kotlin-bibliotheek voor het testen van Flow. Turbine stelt u in staat elke emissie afzonderlijk te controleren met time-outs en voltooiingscontrole. Voorbeeld: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. U kunt ook .toList() gebruiken in runTest met opgave van het aantal verwachte gebeurtenissen.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook