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().

ПараметрТипDefaultОпис
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

Навігаційні команди — класичний 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()
            }
        }
    }
}

Приклад 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 з непереповнюваним буфером (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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також