SharedFlow — un flux reactiv fierbinte din biblioteca Kotlin Coroutines, optimizat pentru evenimente unice (one-shot events) care nu trebuie să se repete la rotirea ecranului sau recrearea abonatului. Arătăm cum se deosebește SharedFlow de StateFlow: spre deosebire de StateFlow, SharedFlow nu stochează ultima valoare pentru noii abonați și suportă configurația replay, extraBufferCapacity și onBufferOverflow. Conform Google (Android Developers, 2025), SharedFlow este soluția recomandată pentru comenzi de navigare, mesaje Snackbar și alte evenimente care trebuie procesate exact o dată.
Principalele
SharedFlow — este un flux fierbinte (hot flow) din biblioteca kotlinx.coroutines.flow care, spre deosebire de StateFlow, nu este legat de o singură stare și poate emite un număr arbitrar de evenimente către orice abonați. SharedFlow este tipul de bază pentru StateFlow — StateFlow este implementat prin SharedFlow cu replay = 1.
Caracteristica cheie a SharedFlow — nu trebuie să stocheze ultima valoare. Implicit (replay = 0), un nou abonat nu primește nimic până când nu este trimis un eveniment nou. Acest lucru face SharedFlow ideal pentru scenarii în care evenimentul trebuie procesat exact o dată: navigare, Snackbar, notificări de sistem, rezultatele scanării codului QR.
SharedFlow a fost stabilizat în kotlinx.coroutines 1.4.0 (noiembrie 2020) împreună cu StateFlow. Conform documentației Kotlin Coroutines (2025), SharedFlow utilizează blocare cu granularitate fină pentru sincronizarea abonaților și asigură scalabilitate liniară până la 1000+ de abonați simultani fără degradarea performanței, confirmată de testele JetBrains.
Alegerea între SharedFlow și StateFlow depinde de semantica datelor transmise: stare (StateFlow) sau eveniment (SharedFlow). Mai jos sunt criterii clare cu exemple.
| Criteriu | SharedFlow | StateFlow |
|---|---|---|
| Semantică | Evenimente unice (navigare, toast, alertă) | Stare UI (listă, încărcare, eroare) |
| Valoare inițială | Nu este necesară | Obligatorie |
| Repetare la abonare | Doar dacă replay > 0 | Întotdeauna ultima valoare |
| Confluare | Nu — evenimentele nu se pierd (dacă bufferul nu este plin) | Da — stochează doar ultima |
| Bufferizare | Configurabilă prin replay + extraBufferCapacity | Doar 1 (replay=1 fix) |
| Utilizare | navigationEvent, showSnackbar, openDialog | items, isLoading, uiState |
Cea mai simplă regulă: dacă datele trebuie afișate la rotirea ecranului — este stare (StateFlow). Dacă la rotirea ecranului evenimentul nu trebuie repetat — este eveniment unic (SharedFlow). De exemplu, „toast cu mesaj de eroare” — SharedFlow: la rotire toastul nu trebuie să apară din nou. „Listă de produse” — StateFlow: la rotire lista trebuie să rămână pe ecran.
În IT Sectr folosim SharedFlow pentru: comenzi de navigare (tranziție la ecran, deschidere deep link), evenimente UI (Snackbar, AlertDialog), notificări de sistem (actualizare date în fundal, rezultat plată), evenimente analitice (logare, urmărire).
MutableSharedFlow — versiunea modificabilă a SharedFlow cu metodele emit() (suspend) și tryEmit() (non-suspend) pentru trimiterea evenimentelor. emit() se suspendă dacă bufferul este plin și onBufferOverflow = SUSPEND. tryEmit() returnează Boolean — dacă evenimentul a fost adăugat cu succes în buffer.
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
}
Parametrii constructorului sunt critici: replay = 0 garantează că evenimentul nu se va repeta pentru un nou abonat; extraBufferCapacity = 10 — buffer pentru cazul trimiterii rapide a evenimentelor înainte ca UI să se aboneze; DROP_OLDEST — strategia la depășire: evenimentele vechi sunt eliminate, cele noi sunt păstrate. Conform Kotlin Coroutines Performance (JetBrains, 2024), SharedFlow cu extraBufferCapacity = 64 procesează peste 100 000 de evenimente pe secundă fără pierderi.
Modelul Event (sau UiEvent) — metoda recomandată de Google pentru transmiterea evenimentelor unice din ViewModel către View. Spre deosebire de stare (StateFlow), evenimentul trebuie procesat exact o dată, iar la rotirea ecranului nu trebuie repetat. SharedFlow cu replay = 0 este ideal pentru această sarcină.
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 ?: "Eroare de formatare"))
}
}
}
}
sealed interface CheckoutEvent {
data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}
În View (Activity/Fragment): abonarea la eveniment trebuie efectuată în lifecycleScope cu repeatOnLifecycle(STATE.STARTED). La fiecare intrare în STARTED abonarea este recreată, dar evenimentul nu se repetă deoarece SharedFlow cu replay=0 l-a eliberat deja. Aceasta garantează că navigarea către ecranul de urmărire a comenzii va avea loc o singură dată, nu la fiecare rotire.
Constructorul MutableSharedFlow primește trei parametri care determină comportamentul bufferului. Configurarea incorectă poate duce la pierderea evenimentelor sau blocarea emit().
| Parametru | Tip | Implicit | Descriere |
|---|---|---|---|
| replay | Int | 0 | Numărul ultimelor evenimente redate noului abonat. 0 = nu reda, 1 = ca StateFlow |
| extraBufferCapacity | Int | 0 | Buffer suplimentar peste replay. Evenimentele sunt stocate într-un buffer circular. 64 — limita recomandată pentru majoritatea scenariilor |
| onBufferOverflow | BufferOverflow | SUSPEND | Strategia la umplerea bufferului: SUSPEND, DROP_OLDEST, DROP_LATEST |
// Configurații pentru diferite scenarii:
// 1. Evenimente UI unice (navigare, toasturi)
val uiEvents = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 5,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
// 2. Flux replay pentru sincronizarea stării (ca StateFlow)
val stateLike = MutableSharedFlow<AppState>(
replay = 1,
extraBufferCapacity = 0
)
// 3. Trimitere de evenimente de înaltă frecvență (analitică, loguri)
val analytics = MutableSharedFlow<AnalyticsEvent>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
Important: extraBufferCapacity + replay = dimensiunea totală a bufferului. Dacă emit() este apelat mai rapid decât abonatul procesează evenimentul, bufferul se umple și se declanșează onBufferOverflow. Pentru evenimente UI, DROP_OLDEST — strategie sigură: evenimentele vechi (navigări deja neactuale) sunt eliminate în favoarea celor noi. Pentru tranzacții financiare folosiți SUSPEND — aceasta garantează că niciun eveniment nu se pierde cu prețul blocării expeditorului.
Comenzile de navigare — caz de utilizare clasic pentru SharedFlow. Fragmentul se abonează la evenimente și execută navigarea. La rotirea ecranului comanda nu se repetă.
// 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
}
// În 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()
}
}
}
}
Scenariu complex: SharedFlow pentru notificări despre evenimente de fundal cu combinarea cu StateFlow pentru 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("Notificare nouă: ${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("Toate notificările au fost marcate ca citite")
}
}
}
În acest exemplu: StateFlow stochează lista de notificări (stare — se păstrează la rotire), SharedFlow emite mesaje toast (evenimente unice — nu se repetă la rotire). Combinarea a două tipuri de Flow — modelul recomandat de Google pentru ViewModel începând cu 2022.
Întrebări frecvente
Da, dacă bufferul este plin și onBufferOverflow = DROP_OLDEST sau DROP_LATEST. SharedFlow nu garantează livrarea fiecărui eveniment — nu este o coadă de mesaje (ca Channel). Dacă aveți nevoie de livrare garantată a tuturor evenimentelor, utilizați Channel cu buffer nedesățibil (UNLIMITED) sau BroadcastChannel (deprecated). Pentru evenimente UI, pierderea evenimentelor învechite (de exemplu, o navigare veche) — este un comportament așteptat, nu o eroare.
Channel — o coadă FIFO în care fiecare eveniment este livrat exact unui abonat (punct-la-punct). SharedFlow — transmisie: fiecare eveniment este livrat TUTUROR abonaților activi. SharedFlow este mai aproape de BroadcastChannel (care este deprecated) și este potrivit pentru scenarii „unu-la-mulți”. Channel — pentru „unu-la-unu” (pool-uri de fire, pipeline). Conform recomandării JetBrains, SharedFlow este înlocuitorul BroadcastChannel pentru toate proiectele noi.
SharedFlow este deja thread-safe — emit() și collect() sunt sincronizate corect. Mai multe fire pot apela emit() fără blocări, iar toți abonații activi vor primi evenimentele în ordinea corectă. tryEmit() este neblocant — returnează false dacă bufferul este plin. Pentru sisteme cu încărcare mare, utilizați tryEmit() cu DROP_OLDEST — aceasta previne blocarea firelor.
SharedFlow fără replay=1 nu stochează ultima valoare — la rotirea ecranului noul abonat nu primește starea curentă, UI rămâne gol. Cu replay=1 SharedFlow se comportă ca StateFlow, dar pierde optimizarea comparației prin equals(), ceea ce cauzează notificări inutile la retrimiterea aceleiași valori. StateFlow — alegerea corectă pentru stare; SharedFlow — pentru evenimente.
Pentru testarea SharedFlow utilizați Turbine — biblioteca Kotlin pentru testarea Flow. Turbine vă permite să verificați fiecare emisie separat cu timeout-uri și verificare a finalizării. Exemplu: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. De asemenea, puteți utiliza .toList() în runTest cu specificarea numărului de evenimente așteptate.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și