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): подписка на event должна выполняться в 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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также