StateFlow и SharedFlow
StateFlow и SharedFlow - горячие разновидности Flow для передачи значений одному или нескольким коллекторам. StateFlow используют для наблюдаемого состояния с текущим значением, а SharedFlow - для настраиваемой широковещательной передачи значений.
Состояние и события
Что такое StateFlow?
StateFlow - горячий Flow, который всегда хранит текущее значение. Новый коллектор сразу получает его, а затем принимает последующие обновления.
Для StateFlow обязательно начальное значение. Поэтому он хорошо подходит для UI state во ViewModel: loading/content/error, состояния формы, выбранного элемента и вычисляемых данных экрана. Текущее значение также можно прочитать через value.
StateFlow использует conflation. Медленный коллектор может пропустить промежуточные обновления, но получит самое новое значение. Обновления, равные предыдущему по equals(), не отправляются, поэтому модели состояния обычно делают неизменяемыми и с корректной реализацией equality.
Наружу следует отдавать read-only flow, оставляя изменение состояния внутри класса. Для атомарного read-modify-write обновления используйте update:
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun selectItem(id: Long) {
_uiState.update { current -> current.copy(selectedItemId = id) }
}
Что такое SharedFlow?
SharedFlow - горячий Flow, который передаёт каждое новое значение всем активным подписчикам. Обязательного начального значения и свойства value у него нет.
Поведение при доставке настраивается параметрами:
replay- сколько предыдущих значений получит новый подписчик;extraBufferCapacity- дополнительный буфер для медленных активных подписчиков;onBufferOverflow- приостанавливать отправителя или отбрасывать старое/новое значение при переполнении буфера.
У MutableSharedFlow() по умолчанию replay и дополнительная ёмкость равны нулю. emit ждёт, пока активные подписчики примут значение, но при отсутствии подписчиков сразу завершается, а значение теряется. Дополнительный буфер не хранит значения, пока подписчиков нет: для этого используется только replay cache.
SharedFlow подходит для сигналов обновления, общих таймеров приложения, websocket updates и некритичных UI effects с явно заданными правилами доставки:
private val _effects = MutableSharedFlow<UiEffect>(
extraBufferCapacity = 1,
)
val effects: SharedFlow<UiEffect> = _effects.asSharedFlow()
При replay = 0 поздний коллектор не получит уже отправленное значение. Увеличение replay меняет это поведение, но может повторно доставить уже обработанный эффект после пересоздания UI. Поэтому replay не является универсальной гарантией exactly-once delivery.
В отличие от SharedFlow, Channel обычно передаёт каждый элемент одному из конкурирующих получателей, а не всем подписчикам. Подробнее о различиях доставки см. в статье Channels.
StateFlow и SharedFlow: сравнение
| Свойство | StateFlow | SharedFlow |
|---|---|---|
| Текущее значение | Обязательно и доступно через value |
Необязательно |
| Начальное значение | Обязательно | Необязательно |
| Новый подписчик | Получает последнее значение | Получает значения из replay cache |
| Conflation | Сравнение через equals() |
Зависит от буфера и overflow policy |
| Типичное применение | Наблюдаемое состояние | Общие сигналы и временные эффекты |
StateFlow - специализированный вариант SharedFlow для состояния: он хранит одно последнее значение, отдаёт его новым подписчикам и применяет equality-based conflation.
Не храните команду навигации или snackbar как простое поле StateFlow, если она должна быть обработана один раз: новый коллектор может выполнить её повторно. Но замена на SharedFlow(replay = 0) создаёт противоположный риск - эффект потеряется, когда активного коллектора нет.
Преобразование холодных Flow через stateIn и shareIn
Каждый коллектор холодного Flow отдельно запускает его upstream producer. Используйте stateIn, когда потребителям нужно общее состояние, и shareIn, когда нужен общий поток значений.
val uiState: StateFlow<UiState> = repository.observeData()
.map(::toUiState)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = UiState.Loading,
)
Переданный scope определяет время жизни shared-потока. SharingStarted.WhileSubscribed(...) запускает upstream при появлении подписчиков и останавливает после их исчезновения, при необходимости с задержкой. Eagerly запускает его сразу, а Lazily - при первом подписчике и оставляет работающим, пока жив scope.
StateFlow и LiveData
LiveData - observable holder из AndroidX, который автоматически прекращает уведомлять observer, когда его LifecycleOwner неактивен. StateFlow относится к Kotlin Coroutines и сам ничего не знает о lifecycle Android.
Flow в UI нужно собирать с учётом lifecycle: через collectAsStateWithLifecycle() в Compose или внутри repeatOnLifecycle() во View system. Обычный launch или launchIn, привязанный только к scope Activity или Fragment, может продолжить collection после остановки UI.
StateFlow естественно сочетается с операторами Flow, например map и combine, coroutine test APIs и общей Kotlin-архитектурой. LiveData по-прежнему часто встречается в существующем View-based коде, но для современного coroutine-based state обычно выбирают StateFlow.
State и events/effects
State описывает, что экран должен показывать сейчас. Оно должно быть воспроизводимым: повторная collection после пересоздания UI должна приводить к корректной отрисовке.
Effects - временные команды: показать snackbar, выполнить навигацию, прокрутить список или открыть системный picker. Отдельный SharedFlow или Channel подходит, если результат некритичен, а поведение без активного коллектора определено заранее.
Критичные результаты обычно следует сводить к долговечному state. Например, сохраните информацию об успешной операции или записанных данных; навигацию или сообщение об успехе можно выполнить после явного решения UI, а не использовать эффект как единственную запись о результате.
sealed interface UiEffect {
data class ShowSnackbar(val message: String) : UiEffect
data object NavigateBack : UiEffect
}
Выбор не сводится к правилу «state - это StateFlow, events - это SharedFlow». Сначала определите, должна ли информация пережить отсутствие или пересоздание UI, а затем выберите примитив и политику буферизации с подходящими гарантиями.