SharedFlow: шта је то, SharedFlow vs StateFlow у Android

Аутор: IT Sectr Објављено: 2026-02-20 Време читања: 9 мин

SharedFlow — врући реактивни ток из библиотеке Kotlin Coroutines, оптимизован за једнократне догађаје (one-shot events) који се не смеју понављати при окретању екрана или поновном креирању претплатника. Показујемо по чему се SharedFlow разликује од StateFlow: за разлику од StateFlow, SharedFlow не чува последњу вредност за нове претплатнике и подржава конфигурацију replay, extraBufferCapacity и onBufferOverflow. Према Google (Android Developers, 2025), SharedFlow је препоручено решење за навигационе команде, Snackbar поруке и друге догађаје који треба да буду обрађени тачно једном.

Главно

  • SharedFlow — hot flow за једнократне догађаје: нови претплатници не добијају претходне вредности без подешавања replay.
  • MutableSharedFlow — изменљива верзија са методама emit() и tryEmit() за слање догађаја.
  • SharedFlow vs StateFlow: SharedFlow не конфилује вредности (може баферовати више), не захтева почетну вредност, погодан за једнократне догађаје.
  • replay — број последњих догађаја који се репродукују новим претплатницима (подразумевано 0).
  • extraBufferCapacity — додатни бафер за догађаје изнад replay, спречава блокирање emit().

Шта је SharedFlow у Kotlin?

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 vs StateFlow: када шта користити

Избор између SharedFlow и StateFlow зависи од семантике података који се преносе: стање (StateFlow) или догађај (SharedFlow). Испод су јасни критеријуми са примерима.

КритеријумSharedFlowStateFlow
СемантикаЈеднократни догађаји (навигација, тост, алерт)Стање UI (листа, учитавање, грешка)
Почетна вредностНије потребнаОбавезна
Понављање при претплатиСамо ако replay > 0Увек последња вредност
КонфлацијаНе — догађаји се не губе (ако бафер није пун)Да — чува само последњу
БаферисањеПодешава се кроз replay + extraBufferCapacityСамо 1 (replay=1 фиксно)
УпотребаnavigationEvent, showSnackbar, openDialogitems, isLoading, uiState

Најједноставније правило: ако подаци треба да буду приказани при окретању екрана — то је стање (StateFlow). Ако се при окретању екрана догађај не сме поновити — то је једнократни догађај (SharedFlow). На пример, „тост са поруком о грешци“ — SharedFlow: при окретању тост не треба поново да се прикаже. „Листа производа“ — StateFlow: при окретању листа треба да остане на екрану.

У IT Sectr користимо SharedFlow за: навигационе команде (прелазак на екран, отварање дубоког линка), UI догађаје (Snackbar, AlertDialog), системска обавештења (ажурирање података у позадини, резултат плаћања), аналитичке догађаје (логирање, праћење).

MutableSharedFlow: emit, tryEmit и баферисање

MutableSharedFlow — изменљива верзија SharedFlow са методама emit() (suspend) и tryEmit() (non-suspend) за слање догађаја. emit() се суспендује ако је бафер пун и onBufferOverflow = SUSPEND. tryEmit() враћа Boolean — да ли је догађај успешно додат у бафер.

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
}

Параметри конструктора су критични: replay = 0 гарантује да се догађај неће поновити за новог претплатника; extraBufferCapacity = 10 — бафер за случај брзог слања догађаја пре него што се UI претплати; DROP_OLDEST — стратегија при препуњавању: стари догађаји се одбацују, нови се чувају. Према Kotlin Coroutines Performance (JetBrains, 2024), SharedFlow са extraBufferCapacity = 64 обрађује преко 100 000 догађаја у секунди без губитака.

SharedFlow за једнократне догађаје: образац Event

Образац Event (или UiEvent) — препоручени Google начин преноса једнократних догађаја из ViewModel у View. За разлику од стања (StateFlow), догађај треба да буде обрађен тачно једном, и при окретању екрана не сме да се понови. SharedFlow са replay = 0 је идеалан за овај задатак.

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 ?: "Грешка форматирања"))
            }
        }
    }
}

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 већ ослободио. Ово гарантује да ће се навигација до екрана за праћење поруџбине десити само једном, а не при сваком окретању.

Параметри SharedFlow: replay, extraBufferCapacity, onBufferOverflow

Конструктор MutableSharedFlow прима три параметра који одређују понашање бафера. Нетачно подешавање може довести до губитка догађаја или блокирања emit().

ПараметарТипПодразумеваноОпис
replayInt0Број последњих догађаја који се репродукују новом претплатнику. 0 = не репродукуј, 1 = као StateFlow
extraBufferCapacityInt0Додатни бафер изнад replay. Догађаји се чувају у кружном баферу. 64 — препоручена граница за већину сценарија
onBufferOverflowBufferOverflowSUSPENDСтратегија при пуњењу бафера: SUSPEND, DROP_OLDEST, DROP_LATEST
kotlin
// Конфигурације за различите сценарије:

// 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 — ово гарантује да ниједан догађај неће бити изгубљен по цену блокирања пошиљаоца.

Примери кода: SharedFlow у Kotlin

Пример 1: SharedFlow за навигацију са Jetpack Navigation

Навигационе команде — класични use case за SharedFlow. Fragment се претплаћује на догађаје и извршава навигацију. При окретању екрана команда се не понавља.

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-у:
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: SharedFlow са Room и Flow операторима

Сложени сценарио: SharedFlow за обавештења о позадинским догађајима са комбиновањем са StateFlow за UI.

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("Ново обавештење: ${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. године.

Често постављана питања

Може ли SharedFlow изгубити догађај?

Да, ако је бафер пун и onBufferOverflow = DROP_OLDEST или DROP_LATEST. SharedFlow не гарантује испоруку сваког догађаја — то није ред порука (као Channel). Ако је потребна гарантована испорука свих догађаја, користите Channel са непрепуњивим бафером (UNLIMITED) или BroadcastChannel (deprecated). За UI догађаје губитак застарелих догађаја (нпр. стара навигација) — очекивано је понашање, а не грешка.

По чему се SharedFlow разликује од Channel?

Channel — FIFO ред у коме се сваки догађај испоручује тачно једном претплатнику (тачка-тачка). SharedFlow — емитовање: сваки догађај се испоручује СВИМ активним претплатницима. SharedFlow је ближи BroadcastChannel (који је deprecated) и погодан је за сценарије „један-према-више“. Channel — за „један-према-један“ (групе нити, pipeline). Према препоруци JetBrains, SharedFlow је замена за BroadcastChannel за све нове пројекте.

Како учинити SharedFlow безбедним за нити?

SharedFlow је већ безбедан за нити — emit() и collect() су правилно синхронизовани. Више нити може позивати emit() без блокирања, а сви активни претплатници ће примити догађаје у исправном редоследу. tryEmit() је неблокирајући — враћа false ако је бафер пун. За системе са високим оптерећењем користите tryEmit() са DROP_OLDEST — ово спречава блокирање нити.

Зашто се SharedFlow не користи за стање?

SharedFlow без replay=1 не чува последњу вредност — при окретању екрана нови претплатник не добија тренутно стање, UI остаје празан. Са replay=1 SharedFlow се понаша као StateFlow, али губи оптимизацију поређења путем equals(), што изазива непотребна обавештења при поновном слању исте вредности. StateFlow — прави избор за стање; SharedFlow — за догађаје.

Како тестирати SharedFlow?

За тестирање SharedFlow користите Turbine — Kotlin библиотеку за тестирање Flow. Turbine вам омогућава да проверите сваку емисију појединачно са тајмаутима и провером завршетка. Пример: viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }. Такође можете користити .toList() у runTest са навођењем броја очекиваних догађаја.

Завршни закључци

  • SharedFlow — врући реактивни ток за једнократне догађаје, невезане за последње стање.
  • SharedFlow vs StateFlow: SharedFlow — догађаји (навигација, тостови, алерти), StateFlow — стање (листе, учитавање, грешке).
  • MutableSharedFlow са replay=0, extraBufferCapacity=5, DROP_OLDEST — стандардна конфигурација за UI догађаје.
  • emit() — suspend функција за блокирајуће слање; tryEmit() — non-suspend са Boolean резултатом.
  • Образац UiEvent са sealed class — препоручени начин преноса једнократних догађаја из ViewModel у View.
  • SharedFlow гарантује испоруку сваком претплатнику, али не гарантује испоруку сваког догађаја при препуњавању бафера.
  • Комбинација SharedFlow + StateFlow у једној ViewModel — оптимални образац за архитектуру која раздваја стање и догађаје.

Развићемо мобилну апликацију под кључ

IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође