SharedFlow je horký reaktivní tok z knihovny Kotlin Coroutines, optimalizovaný pro jednorázové události (one-shot events), které by se neměly opakovat při otočení obrazovky nebo opětovném vytvoření odběratele. Ukazujeme, čím se SharedFlow liší od StateFlow: na rozdíl od StateFlow, SharedFlow neuchovává poslední hodnotu pro nové odběratele a podporuje konfiguraci replay, extraBufferCapacity a onBufferOverflow. Podle Google (Android Developers, 2025) je SharedFlow doporučeným řešením pro navigační příkazy, Snackbar zprávy a další události, které by měly být zpracovány právě jednou.
Hlavní body
SharedFlow je horký tok (hot flow) z knihovny kotlinx.coroutines.flow, který na rozdíl od StateFlow není vázán na jeden stav a může emitovat libovolný počet událostí libovolným odběratelům. SharedFlow je základním typem pro StateFlow — StateFlow je implementován právě přes SharedFlow s replay = 1.
Klíčovou vlastností SharedFlow je, že nemusí uchovávat poslední hodnotu. Ve výchozím nastavení (replay = 0) nový odběratel nic neobdrží, dokud není odeslána nová událost. To dělá SharedFlow ideálním pro scénáře, kde má být událost zpracována právě jednou: navigace, Snackbar, systémová oznámení, výsledky skenování QR kódu.
SharedFlow byl stabilizován v kotlinx.coroutines 1.4.0 (listopad 2020) spolu se StateFlow. Podle dokumentace Kotlin Coroutines (2025) SharedFlow používá jemně granulární zamykání pro synchronizaci odběratelů a poskytuje lineární škálovatelnost až pro 1000+ současných odběratelů bez degradace výkonu, což bylo potvrzeno testy JetBrains.
Výběr mezi SharedFlow a StateFlow závisí na sémantice přenášených dat: stav (StateFlow) nebo událost (SharedFlow). Níže jsou jasná kritéria s příklady.
| Kritérium | SharedFlow | StateFlow |
|---|---|---|
| Sémantika | Jednorázové události (navigace, toast, alert) | Stav UI (seznam, načítání, chyba) |
| Počáteční hodnota | Není vyžadována | Povinná |
| Opakování při odběru | Pouze pokud replay > 0 | Vždy poslední hodnota |
| Slučování | Ne — události se neztrácejí (pokud buffer nepřeteče) | Ano — uchovává pouze poslední |
| Bufferování | Nastavitelné přes replay + extraBufferCapacity | Pouze 1 (replay=1 fixní) |
| Použití | navigationEvent, showSnackbar, openDialog | items, isLoading, uiState |
Nejjednodušší pravidlo: pokud data mají být zobrazena při otočení obrazovky — je to stav (StateFlow). Pokud by se událost při otočení obrazovky neměla opakovat — je to jednorázová událost (SharedFlow). Například «toast s chybovou zprávou» — SharedFlow: při otočení by se toast neměl znovu zobrazit. «Seznam produktů» — StateFlow: při otočení by měl seznam zůstat na obrazovce.
V IT Sectr používáme SharedFlow pro: navigační příkazy (přechod na obrazovku, otevření deep linku), UI události (Snackbar, AlertDialog), systémová oznámení (aktualizace dat na pozadí, výsledek platby), analytické události (logování, tracking).
MutableSharedFlow je měnitelná verze SharedFlow s metodami emit() (suspend) a tryEmit() (non-suspend) pro odesílání událostí. emit() se pozastaví, pokud je buffer plný a onBufferOverflow = SUSPEND. tryEmit() vrací Boolean — zda byla událost úspěšně přidána do bufferu.
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
}
Parametry konstruktoru jsou kriticky důležité: replay = 0 zaručuje, že se událost nezopakuje pro nového odběratele; extraBufferCapacity = 10 — buffer pro případ rychlého odesílání událostí před tím, než se UI přihlásí; DROP_OLDEST — strategie při přetečení: staré události se zahazují, nové se uchovávají. Podle Kotlin Coroutines Performance (JetBrains, 2024) SharedFlow s extraBufferCapacity = 64 zpracovává více než 100 000 událostí za sekundu beze ztrát.
Vzor Event (neboli UiEvent) — doporučený způsob Google pro přenos jednorázových událostí z ViewModel do View. Na rozdíl od stavu (StateFlow) by událost měla být zpracována právě jednou a při otočení obrazovky by se neměla opakovat. SharedFlow s replay = 0 je pro tento úkol ideální.
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 ?: "Chyba objednávky"))
}
}
}
}
sealed interface CheckoutEvent {
data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}
Ve View (Activity/Fragment): odběr události by měl být proveden v lifecycleScope s repeatOnLifecycle(STATE.STARTED). Při každém vstupu do STARTED je odběr vytvořen znovu, ale událost se neopakuje, protože SharedFlow s replay=0 ji již uvolnil. To zaručuje, že navigace na obrazovku sledování objednávky proběhne pouze jednou, ne při každém otočení.
Konstruktor MutableSharedFlow přijímá tři parametry, které určují chování bufferu. Nesprávné nastavení může vést ke ztrátě událostí nebo blokování emit().
| Parametr | Typ | Výchozí | Popis |
|---|---|---|---|
| replay | Int | 0 | Počet posledních událostí přehrávaných novému odběrateli. 0 = nepřehrávat, 1 = jako StateFlow |
| extraBufferCapacity | Int | 0 | Dodatečný buffer nad rámec replay. Události se ukládají do kruhového bufferu. 64 — doporučený limit pro většinu scénářů |
| onBufferOverflow | BufferOverflow | SUSPEND | Strategie při zaplnění bufferu: SUSPEND, DROP_OLDEST, DROP_LATEST |
// Konfigurace pro různé scénáře:
// 1. Jednorázové UI události (navigace, toasty)
val uiEvents = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 5,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
// 2. Replay tok pro synchronizaci stavu (jako StateFlow)
val stateLike = MutableSharedFlow<AppState>(
replay = 1,
extraBufferCapacity = 0
)
// 3. Vysokofrekvenční odesílání událostí (analytika, logy)
val analytics = MutableSharedFlow<AnalyticsEvent>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
Důležité: extraBufferCapacity + replay = celková velikost bufferu. Pokud je emit() volán rychleji, než odběratel zpracovává událost, buffer se zaplní a aktivuje se onBufferOverflow. Pro UI události je DROP_OLDEST bezpečná strategie: staré události (již neaktuální navigace) se zahazují ve prospěch nových. Pro finanční transakce použijte SUSPEND — to zaručuje, že žádná událost není ztracena, i za cenu blokování odesílatele.
Navigační příkazy jsou klasickým případem použití pro SharedFlow. Fragment se přihlásí k odběru událostí a provádí navigaci. Při otočení obrazovky se příkaz neopakuje.
// 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
}
// Ve Fragmentu:
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()
}
}
}
}
Složitý scénář: SharedFlow pro oznámení o událostech na pozadí v kombinaci se StateFlow pro 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("Nová notifikace: ${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("Všechny notifikace označeny jako přečtené")
}
}
}
V tomto příkladu: StateFlow uchovává seznam oznámení (stav — zachovává se při otočení), SharedFlow emituje toast zprávy (jednorázové události — neopakují se při otočení). Kombinace dvou typů Flow je doporučeným vzorem Google pro ViewModel od roku 2022.
Často kladené otázky
Ano, pokud buffer přeteče a onBufferOverflow = DROP_OLDEST nebo DROP_LATEST. SharedFlow negarantuje doručení každé události — není to fronta zpráv (jako Channel). Pokud je potřeba zaručené doručení všech událostí, použijte Channel s nepřetečitelným bufferem (UNLIMITED) nebo BroadcastChannel (deprecated). Pro UI události je ztráta zastaralých událostí (např. stará navigace) očekávaným chováním, nikoli chybou.
Channel je FIFO fronta, kde je každá událost doručena právě jednomu odběrateli (bod-bod). SharedFlow je vysílání: každá událost je doručena VŠEM aktivním odběratelům. SharedFlow je blíže BroadcastChannel (který je deprecated) a hodí se pro scénáře «jeden-k-mnoha». Channel je pro «jeden-k-jednomu» (fondy vláken, pipeline). Podle doporučení JetBrains je SharedFlow náhradou za BroadcastChannel pro všechny nové projekty.
SharedFlow je již vláknově bezpečný — emit() a collect() jsou správně synchronizovány. Více vláken může volat emit() bez zamykání a všichni aktivní odběratelé obdrží události ve správném pořadí. tryEmit() je neblokující — vrací false, pokud je buffer plný. Pro vysoce zatížené systémy použijte tryEmit() s DROP_OLDEST — to zabraňuje blokování vláken.
SharedFlow bez replay=1 neuchovává poslední hodnotu — při otočení obrazovky nový odběratel nezíská aktuální stav, UI zůstane prázdné. S replay=1 se SharedFlow chová jako StateFlow, ale ztrácí optimalizaci porovnání přes equals(), což způsobuje zbytečná oznámení při opakovaném odeslání stejné hodnoty. StateFlow je správnou volbou pro stav; SharedFlow pro události.
Pro testování SharedFlow použijte Turbine — Kotlin knihovnu pro testování Flow. Turbine umožňuje kontrolovat každou emisi samostatně s timeouty a kontrolou dokončení. Příklad: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. Lze také použít .toList() v runTest s uvedením počtu očekávaných událostí.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také