SharedFlow: mi ez, SharedFlow vs StateFlow Android rendszeren

Szerző: IT Sectr Megjelenés: 2026-02-20 Olvasási idő: 9 perc

SharedFlow — egy forró reaktív adatfolyam a Kotlin Coroutines könyvtárból, optimalizálva egyszeri eseményekhez (one-shot events), amelyek nem ismétlődhetnek meg a képernyő elforgatásakor vagy az előfizető újbóli létrehozásakor. Megmutatjuk, miben különbözik a SharedFlow a StateFlow-tól: a StateFlow-val ellentétben a SharedFlow nem tárolja az utolsó értéket az új előfizetők számára, és támogatja a replay, extraBufferCapacity és onBufferOverflow konfigurációját. A Google (Android Developers, 2025) szerint a SharedFlow az ajánlott megoldás navigációs parancsokhoz, Snackbar üzenetekhez és más olyan eseményekhez, amelyeket pontosan egyszer kell feldolgozni.

Főbb pontok

  • SharedFlow — hot flow egyszeri eseményekhez: az új előfizetők nem kapják meg az előző értékeket a replay beállítása nélkül.
  • MutableSharedFlow — módosítható verzió az emit() és tryEmit() metódusokkal események küldéséhez.
  • SharedFlow vs StateFlow: A SharedFlow nem konflálja az értékeket (többet is pufferelhet), nem igényel kezdőértéket, alkalmas egyszeri eseményekhez.
  • replay — az új előfizetők számára lejátszott utolsó események száma (alapértelmezett 0).
  • extraBufferCapacity — további puffer a replay-en felüli eseményekhez, megakadályozza az emit() blokkolását.

Mi az a SharedFlow Kotlinban?

SharedFlow — egy forró adatfolyam (hot flow) a kotlinx.coroutines.flow könyvtárból, amely a StateFlow-val ellentétben nem kötődik egyetlen állapothoz, és tetszőleges számú eseményt képes küldeni tetszőleges előfizetőknek. A SharedFlow az alap típus a StateFlow számára — a StateFlow a SharedFlow-on keresztül van megvalósítva replay = 1 értékkel.

A SharedFlow kulcsjellemzője — nem kötelező tárolnia az utolsó értéket. Alapértelmezés szerint (replay = 0) az új előfizető semmit sem kap, amíg új eseményt nem küldenek. Ez teszi a SharedFlow-t ideálissá olyan forgatókönyvekhez, ahol az eseményt pontosan egyszer kell feldolgozni: navigáció, Snackbar, rendszerértesítések, QR-kód beolvasásának eredményei.

A SharedFlow a kotlinx.coroutines 1.4.0-ban (2020. november) stabilizálódott a StateFlow-val együtt. A Kotlin Coroutines dokumentáció (2025) szerint a SharedFlow finomszemcsés zárolást használ az előfizetők szinkronizálásához, és lineáris skálázhatóságot biztosít akár 1000+ egyidejű előfizetőig teljesítményromlás nélkül, amit a JetBrains tesztek is megerősítenek.

SharedFlow vs StateFlow: mikor mit használjunk

A SharedFlow és StateFlow közötti választás a továbbított adatok szemantikájától függ: állapot (StateFlow) vagy esemény (SharedFlow). Az alábbiakban egyértelmű kritériumokat talál példákkal.

KritériumSharedFlowStateFlow
SzemantikaEgyszeri események (navigáció, toast, riasztás)UI állapot (lista, betöltés, hiba)
KezdőértékNem szükségesKötelező
Ismétlés feliratkozáskorCsak ha replay > 0Mindig az utolsó érték
KonflációNem — az események nem vesznek el (ha a puffer nem telt meg)Igen — csak az utolsót tárolja
PufferelésKonfigurálható replay + extraBufferCapacity segítségévelCsak 1 (replay=1 rögzített)
HasználatnavigationEvent, showSnackbar, openDialogitems, isLoading, uiState

A legegyszerűbb szabály: ha az adatoknak láthatónak kell lenniük a képernyő elforgatásakor — az állapot (StateFlow). Ha a képernyő elforgatásakor az esemény nem ismétlődhet meg — az egyszeri esemény (SharedFlow). Például „toast hibaüzenettel” — SharedFlow: elforgatáskor a toast nem jelenhet meg újra. „Terméklista” — StateFlow: elforgatáskor a listának a képernyőn kell maradnia.

Az IT Sectr-nél a SharedFlow-t használjuk: navigációs parancsokhoz (képernyőre váltás, deep link megnyitása), UI eseményekhez (Snackbar, AlertDialog), rendszerértesítésekhez (háttéradatok frissítése, fizetési eredmény), analitikai eseményekhez (naplózás, követés).

MutableSharedFlow: emit, tryEmit és pufferelés

MutableSharedFlow — a SharedFlow módosítható verziója az emit() (suspend) és tryEmit() (non-suspend) metódusokkal események küldéséhez. Az emit() felfüggeszti magát, ha a puffer tele van és onBufferOverflow = SUSPEND. A tryEmit() Boolean értéket ad vissza — hogy az esemény sikeresen hozzáadódott-e a pufferhez.

kotlin
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
}

A konstruktor paraméterei kritikusak: a replay = 0 garantálja, hogy az esemény nem ismétlődik meg egy új előfizető számára; extraBufferCapacity = 10 — puffer a gyors eseményküldéshez, mielőtt a UI feliratkozna; DROP_OLDEST — túlcsordulási stratégia: a régi események eldobásra kerülnek, az újak megmaradnak. A Kotlin Coroutines Performance (JetBrains, 2024) szerint a SharedFlow extraBufferCapacity = 64 értékkel több mint 100 000 eseményt dolgoz fel másodpercenként veszteség nélkül.

SharedFlow egyszeri eseményekhez: az Event minta

Az Event (vagy UiEvent) minta — a Google által ajánlott módszer egyszeri események ViewModel-ből View-ba történő továbbítására. Az állapottal (StateFlow) ellentétben az eseményt pontosan egyszer kell feldolgozni, és a képernyő elforgatásakor nem szabad megismétlődnie. A replay = 0 értékű SharedFlow ideális ehhez a feladathoz.

kotlin
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 ?: "Formázási hiba"))
            }
        }
    }
}

sealed interface CheckoutEvent {
    data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
    data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}

A View-ban (Activity/Fragment): az eseményre való feliratkozást a lifecycleScope-ban kell végrehajtani a repeatOnLifecycle(STATE.STARTED) segítségével. Minden STARTED-be történő belépéskor a feliratkozás újra létrejön, de az esemény nem ismétlődik, mert a SharedFlow replay=0-val már felszabadította azt. Ez garantálja, hogy a rendeléskövető képernyőre történő navigáció csak egyszer történik meg, nem minden elforgatáskor.

SharedFlow paraméterek: replay, extraBufferCapacity, onBufferOverflow

A MutableSharedFlow konstruktora három paramétert fogad, amelyek meghatározzák a puffer viselkedését. A helytelen konfiguráció események elvesztéséhez vagy az emit() blokkolásához vezethet.

ParaméterTípusAlapértelmezettLeírás
replayInt0Az új előfizető számára lejátszott utolsó események száma. 0 = ne játssza le, 1 = mint a StateFlow
extraBufferCapacityInt0További puffer a replay-en felül. Az események körpufferben tárolódnak. 64 — ajánlott határ a legtöbb forgatókönyvhöz
onBufferOverflowBufferOverflowSUSPENDStratégia puffer túlcsorduláskor: SUSPEND, DROP_OLDEST, DROP_LATEST
kotlin
// Konfigurációk különböző forgatókönyvekhez:

// 1. Egyszeri UI események (navigáció, toastok)
val uiEvents = MutableSharedFlow<UiEvent>(
    replay = 0,
    extraBufferCapacity = 5,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

// 2. Replay adatfolyam állapot szinkronizálásához (mint a StateFlow)
val stateLike = MutableSharedFlow<AppState>(
    replay = 1,
    extraBufferCapacity = 0
)

// 3. Nagy frekvenciájú eseményküldés (analitika, naplók)
val analytics = MutableSharedFlow<AnalyticsEvent>(
    replay = 0,
    extraBufferCapacity = 100,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

Fontos: extraBufferCapacity + replay = a puffer teljes mérete. Ha az emit() gyorsabban kerül meghívásra, mint ahogy az előfizető feldolgozza az eseményt, a puffer megtelik és az onBufferOverflow aktiválódik. UI eseményekhez a DROP_OLDEST biztonságos stratégia: a régi események (már nem aktuális navigációk) eldobásra kerülnek az újak javára. Pénzügyi tranzakciókhoz használja a SUSPEND-et — ez garantálja, hogy egyetlen esemény sem vész el a feladó blokkolása árán.

Kódpéldák: SharedFlow Kotlinban

1. példa: SharedFlow navigációhoz Jetpack Navigation-nel

Navigációs parancsok — klasszikus use case a SharedFlow számára. A Fragment feliratkozik az eseményekre és végrehajtja a navigációt. A képernyő elforgatásakor a parancs nem ismétlődik.

kotlin
// 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
}

// Fragment-ben:
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()
            }
        }
    }
}

2. példa: SharedFlow Room-mal és Flow operátorokkal

Összetett forgatókönyv: SharedFlow a háttéreseményekről szóló értesítésekhez, kombinálva StateFlow-val az UI számára.

kotlin
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("Új értesítés: ${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("Minden értesítés olvasottként jelölve")
        }
    }
}

Ebben a példában: a StateFlow tárolja az értesítések listáját (állapot — megmarad elforgatáskor), a SharedFlow toast üzeneteket küld (egyszeri események — nem ismétlődnek elforgatáskor). Két Flow típus kombinációja — a Google által ajánlott minta a ViewModel számára 2022 óta.

Gyakran Ismételt Kérdések

Elveszítheti a SharedFlow az eseményt?

Igen, ha a puffer megtelt és az onBufferOverflow = DROP_OLDEST vagy DROP_LATEST. A SharedFlow nem garantálja minden esemény kézbesítését — ez nem egy üzenetsor (mint a Channel). Ha az összes esemény garantált kézbesítésére van szükség, használjon Channel-t nem túlcsorduló pufferrel (UNLIMITED) vagy BroadcastChannel-t (deprecated). UI eseményeknél az elavult események elvesztése (pl. régi navigáció) — várható viselkedés, nem hiba.

Miben különbözik a SharedFlow a Channel-től?

Channel — egy FIFO sor, ahol minden esemény pontosan egy előfizetőnek kerül kézbesítésre (pont-pont). SharedFlow — sugárzás: minden esemény az ÖSSZES aktív előfizetőnek kerül kézbesítésre. A SharedFlow közelebb áll a BroadcastChannel-hez (ami deprecated), és alkalmas az „egy-többhöz” forgatókönyvekhez. Channel — az „egy-az-egyhez” esetekre (szálpoolok, pipeline). A JetBrains ajánlása szerint a SharedFlow a BroadcastChannel helyettesítője minden új projektben.

Hogyan tehető a SharedFlow thread-safe-é?

A SharedFlow már thread-safe — az emit() és a collect() megfelelően szinkronizáltak. Több szál is hívhatja az emit()-et blokkolás nélkül, és az összes aktív előfizető megkapja az eseményeket a helyes sorrendben. A tryEmit() nem blokkoló — false-t ad vissza, ha a puffer tele van. Nagy terhelésű rendszerekhez használja a tryEmit()-et DROP_OLDEST-tel — ez megakadályozza a szálak blokkolását.

Miért nem használják a SharedFlow-t állapothoz?

A SharedFlow replay=1 nélkül nem tárolja az utolsó értéket — a képernyő elforgatásakor az új előfizető nem kapja meg az aktuális állapotot, az UI üres marad. A replay=1 értékkel a SharedFlow úgy viselkedik, mint a StateFlow, de elveszíti az equals()-en keresztüli összehasonlítás optimalizálását, ami szükségtelen értesítéseket okoz ugyanazon érték újbóli elküldésekor. A StateFlow — a helyes választás állapothoz; a SharedFlow — eseményekhez.

Hogyan teszteljük a SharedFlow-t?

A SharedFlow teszteléséhez használja a Turbine-t — a Flow tesztelésére szolgáló Kotlin könyvtárat. A Turbine lehetővé teszi minden egyes emisszió külön-külön történő ellenőrzését időtúllépésekkel és befejezési ellenőrzéssel. Példa: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. Használhatja a .toList()-et is a runTest-ben a várt események számának megadásával.

Összefoglalás

  • SharedFlow — forró reaktív adatfolyam egyszeri eseményekhez, nem kötődik az utolsó állapothoz.
  • SharedFlow vs StateFlow: SharedFlow — események (navigáció, toastok, riasztások), StateFlow — állapot (listák, betöltés, hibák).
  • MutableSharedFlow replay=0, extraBufferCapacity=5, DROP_OLDEST — standard konfiguráció UI eseményekhez.
  • emit() — suspend függvény blokkoló küldéshez; tryEmit() — non-suspend Boolean eredménnyel.
  • Az UiEvent minta sealed class-szal — ajánlott módszer egyszeri események ViewModel-ből View-ba történő továbbítására.
  • A SharedFlow garantálja a kézbesítést minden előfizetőnek, de nem garantálja minden esemény kézbesítését puffer túlcsorduláskor.
  • A SharedFlow + StateFlow kombinációja egy ViewModel-ben — optimális minta az állapotot és eseményeket elkülönítő architektúrához.

Kulcsrakész mobilalkalmazást fejlesztünk

Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.

Projekt megbeszélése

Olvassa el is