SharedFlow: co to je, SharedFlow vs StateFlow v Android

Autor: IT Sectr Publikováno: 2026-02-20 Doba čtení: 9 min

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 — hot flow pro jednorázové události: noví odběratelé bez nastavení replay nedostanou předchozí hodnoty.
  • MutableSharedFlow — měnitelná verze s metodami emit() a tryEmit() pro odesílání událostí.
  • SharedFlow vs StateFlow: SharedFlow neslučuje hodnoty (může jich několik bufferovat), nevyžaduje počáteční hodnotu, je vhodný pro jednorázové události.
  • replay — počet posledních událostí přehrávaných novému odběrateli (výchozí 0).
  • extraBufferCapacity — dodatečný buffer pro události nad rámec replay, zabraňující blokování emit().

Co je SharedFlow v Kotlin?

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.

SharedFlow vs StateFlow: kdy co použít

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ériumSharedFlowStateFlow
SémantikaJednorázové události (navigace, toast, alert)Stav UI (seznam, načítání, chyba)
Počáteční hodnotaNení vyžadovánaPovinná
Opakování při odběruPouze pokud replay > 0Vž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 + extraBufferCapacityPouze 1 (replay=1 fixní)
PoužitínavigationEvent, showSnackbar, openDialogitems, 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: emit, tryEmit a bufferování

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.

SharedFlow pro jednorázové události: vzor Event

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í.

Parametry SharedFlow: replay, extraBufferCapacity, onBufferOverflow

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().

ParametrTypVýchozíPopis
replayInt0Počet posledních událostí přehrávaných novému odběrateli. 0 = nepřehrávat, 1 = jako StateFlow
extraBufferCapacityInt0Dodateč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ářů
onBufferOverflowBufferOverflowSUSPENDStrategie 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.

Příklady kódu: SharedFlow v Kotlin

Příklad 1: SharedFlow pro navigaci s Jetpack Navigation

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()
            }
        }
    }
}

Příklad 2: SharedFlow s Room a Flow operátory

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

Může SharedFlow ztratit událost?

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.

Čím se SharedFlow liší od Channel?

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.

Jak udělat SharedFlow vláknově bezpečný?

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.

Proč se SharedFlow nepoužívá pro stav?

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.

Jak testovat SharedFlow?

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í

  • SharedFlow — horký reaktivní tok pro jednorázové události, nezávislý na posledním stavu.
  • SharedFlow vs StateFlow: SharedFlow — události (navigace, toasty, alerty), StateFlow — stav (seznamy, načítání, chyby).
  • MutableSharedFlow s replay=0, extraBufferCapacity=5, DROP_OLDEST — standardní konfigurace pro UI události.
  • emit() — suspend funkce pro blokující odesílání; tryEmit() — non-suspend s Boolean výsledkem.
  • Vzor UiEvent s sealed class — doporučený způsob přenosu jednorázových událostí z ViewModel do View.
  • SharedFlow garantuje doručení každému odběrateli, ale negarantuje doručení každé události při přetečení bufferu.
  • Kombinace SharedFlow + StateFlow v jedné ViewModel — optimální vzor pro architekturu oddělující stav a události.

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í.

Prodiskutovat projekt

Přečtěte si také