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 — 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.
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érium | SharedFlow | StateFlow |
|---|---|---|
| Szemantika | Egyszeri események (navigáció, toast, riasztás) | UI állapot (lista, betöltés, hiba) |
| Kezdőérték | Nem szükséges | Kötelező |
| Ismétlés feliratkozáskor | Csak ha replay > 0 | Mindig 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és | Konfigurálható replay + extraBufferCapacity segítségével | Csak 1 (replay=1 rögzített) |
| Használat | navigationEvent, showSnackbar, openDialog | items, 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 — 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.
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.
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.
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.
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éter | Típus | Alapértelmezett | Leírás |
|---|---|---|---|
| replay | Int | 0 | Az új előfizető számára lejátszott utolsó események száma. 0 = ne játssza le, 1 = mint a StateFlow |
| extraBufferCapacity | Int | 0 | Tová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 |
| onBufferOverflow | BufferOverflow | SUSPEND | Stratégia puffer túlcsorduláskor: SUSPEND, DROP_OLDEST, DROP_LATEST |
// 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.
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.
// 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()
}
}
}
}
Ö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.
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
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.
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.
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.
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.
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
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.
Olvassa el is