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 не конфлипсует значения (может буферизовать несколько), не требует начального значения, подходит для one-shot событий.
  • 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): подписка на event должна выполняться в 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
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 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

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