SharedFlow — врући реактивни ток из библиотеке Kotlin Coroutines, оптимизован за једнократне догађаје (one-shot events) који се не смеју понављати при окретању екрана или поновном креирању претплатника. Показујемо по чему се SharedFlow разликује од StateFlow: за разлику од StateFlow, SharedFlow не чува последњу вредност за нове претплатнике и подржава конфигурацију replay, extraBufferCapacity и onBufferOverflow. Према Google (Android Developers, 2025), SharedFlow је препоручено решење за навигационе команде, Snackbar поруке и друге догађаје који треба да буду обрађени тачно једном.
Главно
SharedFlow — је врући ток (hot flow) из библиотеке kotlinx.coroutines.flow који, за разлику од StateFlow, није везан за једно стање и може емитовати произвољан број догађаја произвољним претплатницима. SharedFlow је основни тип за StateFlow — StateFlow је имплементиран кроз SharedFlow са replay = 1.
Кључна карактеристика SharedFlow — не мора да чува последњу вредност. Подразумевано (replay = 0) нови претплатник не добија ништа док се не пошаље нови догађај. То чини SharedFlow идеалним за сценарије где догађај треба да буде обрађен тачно једном: навигација, Snackbar, системска обавештења, резултати скенирања QR кода.
SharedFlow је стабилизован у kotlinx.coroutines 1.4.0 (новембар 2020) заједно са StateFlow. Према документацији Kotlin Coroutines (2025), SharedFlow користи фино гранулисано закључавање за синхронизацију претплатника и обезбеђује линеарну скалабилност до 1000+ истовремених претплатника без деградације перформанси, што је потврђено JetBrains тестовима.
Избор између SharedFlow и StateFlow зависи од семантике података који се преносе: стање (StateFlow) или догађај (SharedFlow). Испод су јасни критеријуми са примерима.
| Критеријум | SharedFlow | StateFlow |
|---|---|---|
| Семантика | Једнократни догађаји (навигација, тост, алерт) | Стање UI (листа, учитавање, грешка) |
| Почетна вредност | Није потребна | Обавезна |
| Понављање при претплати | Само ако replay > 0 | Увек последња вредност |
| Конфлација | Не — догађаји се не губе (ако бафер није пун) | Да — чува само последњу |
| Баферисање | Подешава се кроз replay + extraBufferCapacity | Само 1 (replay=1 фиксно) |
| Употреба | navigationEvent, showSnackbar, openDialog | items, isLoading, uiState |
Најједноставније правило: ако подаци треба да буду приказани при окретању екрана — то је стање (StateFlow). Ако се при окретању екрана догађај не сме поновити — то је једнократни догађај (SharedFlow). На пример, „тост са поруком о грешци“ — SharedFlow: при окретању тост не треба поново да се прикаже. „Листа производа“ — StateFlow: при окретању листа треба да остане на екрану.
У IT Sectr користимо SharedFlow за: навигационе команде (прелазак на екран, отварање дубоког линка), UI догађаје (Snackbar, AlertDialog), системска обавештења (ажурирање података у позадини, резултат плаћања), аналитичке догађаје (логирање, праћење).
MutableSharedFlow — изменљива верзија SharedFlow са методама emit() (suspend) и tryEmit() (non-suspend) за слање догађаја. emit() се суспендује ако је бафер пун и onBufferOverflow = SUSPEND. tryEmit() враћа Boolean — да ли је догађај успешно додат у бафер.
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
}
Параметри конструктора су критични: replay = 0 гарантује да се догађај неће поновити за новог претплатника; extraBufferCapacity = 10 — бафер за случај брзог слања догађаја пре него што се UI претплати; DROP_OLDEST — стратегија при препуњавању: стари догађаји се одбацују, нови се чувају. Према Kotlin Coroutines Performance (JetBrains, 2024), SharedFlow са extraBufferCapacity = 64 обрађује преко 100 000 догађаја у секунди без губитака.
Образац Event (или UiEvent) — препоручени Google начин преноса једнократних догађаја из ViewModel у View. За разлику од стања (StateFlow), догађај треба да буде обрађен тачно једном, и при окретању екрана не сме да се понови. SharedFlow са replay = 0 је идеалан за овај задатак.
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 ?: "Грешка форматирања"))
}
}
}
}
sealed interface CheckoutEvent {
data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}
У View (Activity/Fragment): претплата на догађај треба да се изврши у lifecycleScope са repeatOnLifecycle(STATE.STARTED). При сваком уласку у STARTED претплата се поново креира, али се догађај не понавља јер га је SharedFlow са replay=0 већ ослободио. Ово гарантује да ће се навигација до екрана за праћење поруџбине десити само једном, а не при сваком окретању.
Конструктор MutableSharedFlow прима три параметра који одређују понашање бафера. Нетачно подешавање може довести до губитка догађаја или блокирања emit().
| Параметар | Тип | Подразумевано | Опис |
|---|---|---|---|
| replay | Int | 0 | Број последњих догађаја који се репродукују новом претплатнику. 0 = не репродукуј, 1 = као StateFlow |
| extraBufferCapacity | Int | 0 | Додатни бафер изнад replay. Догађаји се чувају у кружном баферу. 64 — препоручена граница за већину сценарија |
| onBufferOverflow | BufferOverflow | SUSPEND | Стратегија при пуњењу бафера: SUSPEND, DROP_OLDEST, DROP_LATEST |
// Конфигурације за различите сценарије:
// 1. Једнократни UI догађаји (навигација, тостови)
val uiEvents = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 5,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
// 2. Replay ток за синхронизацију стања (као StateFlow)
val stateLike = MutableSharedFlow<AppState>(
replay = 1,
extraBufferCapacity = 0
)
// 3. Високофреквентно слање догађаја (аналитика, логови)
val analytics = MutableSharedFlow<AnalyticsEvent>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
Важно: extraBufferCapacity + replay = укупна величина бафера. Ако се emit() позива брже него што претплатник обрађује догађај, бафер се пуни и покреће се onBufferOverflow. За UI догађаје DROP_OLDEST — безбедна стратегија: стари догађаји (више неактуелне навигације) се одбацују у корист нових. За финансијске трансакције користите SUSPEND — ово гарантује да ниједан догађај неће бити изгубљен по цену блокирања пошиљаоца.
Навигационе команде — класични use case за SharedFlow. Fragment се претплаћује на догађаје и извршава навигацију. При окретању екрана команда се не понавља.
// 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-у:
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()
}
}
}
}
Сложени сценарио: SharedFlow за обавештења о позадинским догађајима са комбиновањем са StateFlow за 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("Ново обавештење: ${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("Сва обавештења су означена као прочитана")
}
}
}
У овом примеру: StateFlow чува листу обавештења (стање — чува се при окретању), SharedFlow емитује тост поруке (једнократни догађаји — не понављају се при окретању). Комбинација два типа Flow — препоручени Google образац за ViewModel од 2022. године.
Често постављана питања
Да, ако је бафер пун и onBufferOverflow = DROP_OLDEST или DROP_LATEST. SharedFlow не гарантује испоруку сваког догађаја — то није ред порука (као Channel). Ако је потребна гарантована испорука свих догађаја, користите Channel са непрепуњивим бафером (UNLIMITED) или BroadcastChannel (deprecated). За UI догађаје губитак застарелих догађаја (нпр. стара навигација) — очекивано је понашање, а не грешка.
Channel — FIFO ред у коме се сваки догађај испоручује тачно једном претплатнику (тачка-тачка). SharedFlow — емитовање: сваки догађај се испоручује СВИМ активним претплатницима. SharedFlow је ближи BroadcastChannel (који је deprecated) и погодан је за сценарије „један-према-више“. Channel — за „један-према-један“ (групе нити, pipeline). Према препоруци JetBrains, SharedFlow је замена за BroadcastChannel за све нове пројекте.
SharedFlow је већ безбедан за нити — emit() и collect() су правилно синхронизовани. Више нити може позивати emit() без блокирања, а сви активни претплатници ће примити догађаје у исправном редоследу. tryEmit() је неблокирајући — враћа false ако је бафер пун. За системе са високим оптерећењем користите tryEmit() са DROP_OLDEST — ово спречава блокирање нити.
SharedFlow без replay=1 не чува последњу вредност — при окретању екрана нови претплатник не добија тренутно стање, UI остаје празан. Са replay=1 SharedFlow се понаша као StateFlow, али губи оптимизацију поређења путем equals(), што изазива непотребна обавештења при поновном слању исте вредности. StateFlow — прави избор за стање; SharedFlow — за догађаје.
За тестирање SharedFlow користите Turbine — Kotlin библиотеку за тестирање Flow. Turbine вам омогућава да проверите сваку емисију појединачно са тајмаутима и провером завршетка. Пример: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. Такође можете користити .toList() у runTest са навођењем броја очекиваних догађаја.
Завршни закључци
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође