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 — това гарантира, че нито едно събитие няма да се загуби, дори с цената на блокиране на изпращача.
Навигационните команди са класически случай на употреба за 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 с непрeпълващ се буфер (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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също