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().
| Параметр | Тип | Default | Опис |
|---|---|---|---|
| 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також