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 — дали събитието е добавено успешно в буфера.

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 е идеален за тази задача.

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
// Конфигурации за различни сценарии:

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

Навигационните команди са класически случай на употреба за 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()
            }
        }
    }
}

Пример 2: SharedFlow с Room и Flow оператори

Сложен сценарий: 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 г. насам.

Често задавани въпроси

Може ли SharedFlow да загуби събитие?

Да, ако буферът е препълнен и onBufferOverflow = DROP_OLDEST или DROP_LATEST. SharedFlow не гарантира доставка на всяко събитие — това не е опашка от съобщения (като Channel). Ако е необходима гарантирана доставка на всички събития, използвайте Channel с непрeпълващ се буфер (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 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също