--- title: StateFlow и SharedFlow description: Горячие потоки: хранение состояния, события, replay, stateIn и shareIn, конфляция и выбор между ними tags: - kotlin - coroutines - flow --- `StateFlow` и `SharedFlow` - **горячие** потоки: они существуют и работают независимо от подписчиков. Обычный `Flow` холодный - он запускается на каждый `collect` и умирает вместе с ним. ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun main() = runBlocking { val state = MutableStateFlow(0) val job = launch { state.collect { println("подписчик: $it") } } delay(20) state.value = 1 state.value = 2 delay(20) job.cancel() println("текущее значение без подписчиков: ${state.value}") // подписчик: 0 // подписчик: 1 // подписчик: 2 // текущее значение без подписчиков: 2 } ``` Обратите внимание: подписчик сразу получил текущее значение `0`, а после отмены сбора состояние осталось доступным через `.value`. ## StateFlow: состояние `StateFlow` - это наблюдаемое значение. У него всегда есть текущее состояние, и оно всегда одно. ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* class Counter { private val _count = MutableStateFlow(0) val count: StateFlow = _count.asStateFlow() // только чтение снаружи fun increment() { _count.value++ } fun add(n: Int) = _count.update { it + n } // атомарно } fun main() = runBlocking { val counter = Counter() val job = launch { counter.count.collect { println("счёт: $it") } } delay(20) counter.increment() delay(20) counter.add(10) delay(20) job.cancel() // счёт: 0 // счёт: 1 // счёт: 11 } ``` Пара «приватный `MutableStateFlow` + публичный `StateFlow`» - основной шаблон: изменять состояние может только владелец, читать - все. ## StateFlow конфлюирует и дедуплицирует Два свойства, которые нужно знать наизусть. **1. Одинаковые подряд значения не доходят** - `StateFlow` сравнивает через `equals`: ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun main() = runBlocking { val state = MutableStateFlow("a") val received = mutableListOf() val job = launch { state.collect { received += it } } delay(20) state.value = "a"; delay(20) // то же значение — не придёт state.value = "b"; delay(20) state.value = "b"; delay(20) // снова дубль state.value = "a"; delay(20) job.cancel() println(received) // [a, b, a] } ``` Задержки между присваиваниями здесь не для красоты: без них подписчик не успел бы обработать значения и сработало бы второе свойство - конфляция (см. ниже), из-за которой он увидел бы только `a` и финальное значение. Это бесплатный `distinctUntilChanged`, но он же источник ошибок с изменяемыми объектами: ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* class MutableBox(val items: MutableList) fun main() = runBlocking { val box = MutableBox(mutableListOf(1)) val state = MutableStateFlow(box) var updates = 0 val job = launch { state.collect { updates++ } } delay(20) box.items.add(2) // изменили содержимое state.value = box // тот же объект — equals true delay(20) job.cancel() println("обновлений: $updates") // обновлений: 1 } ``` Подписчик не узнал об изменении. Поэтому состояние в `StateFlow` должно быть неизменяемым - `data class` с `val` и неизменяемыми коллекциями. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) **2. Медленный подписчик пропускает промежуточные значения:** ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun main() = runBlocking { val state = MutableStateFlow(0) val received = mutableListOf() val job = launch { state.collect { delay(50); received += it } } delay(10) repeat(10) { state.value = it + 1 } delay(150) job.cancel() println("получено не всё: ${received.size < 10}") println("последнее значение дошло: ${received.last() == 10}") // получено не всё: true // последнее значение дошло: true } ``` `StateFlow` - про актуальное состояние, а не про историю. Последнее значение дойдёт всегда, промежуточные - нет. ## MutableStateFlow.update Присваивание `value` не атомарно при конкуренции: ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun main() = runBlocking { val unsafe = MutableStateFlow(0) withContext(Dispatchers.Default) { List(100) { launch { repeat(100) { unsafe.value = unsafe.value + 1 } } } .forEach { it.join() } } println("потеряны обновления: ${unsafe.value < 10_000}") val safe = MutableStateFlow(0) withContext(Dispatchers.Default) { List(100) { launch { repeat(100) { safe.update { v -> v + 1 } } } } .forEach { it.join() } } println("update атомарен: ${safe.value == 10_000}") // потеряны обновления: true // update атомарен: true } ``` `update` выполняет CAS-цикл, поэтому при конкурентных изменениях нужен именно он. Подробнее: [Разделяемое состояние](Разделяемое%20состояние.md) ## SharedFlow: события `SharedFlow` не хранит «текущее значение» - он транслирует события всем подписчикам. ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun main() = runBlocking { val events = MutableSharedFlow() val a = launch { events.collect { println("A: $it") } } val b = launch { events.collect { println("B: $it") } } delay(20) // дать подписчикам стартовать events.emit("событие 1") delay(20) a.cancel(); b.cancel() // A: событие 1 // B: событие 1 } ``` По умолчанию `replay = 0`: подписчик получает только то, что произошло **после** подписки. Событие, отправленное без подписчиков, исчезает: ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun main() = runBlocking { val events = MutableSharedFlow() events.emit("в пустоту") // подписчиков нет val received = mutableListOf() val job = launch { events.collect { received += it } } delay(20) events.emit("услышано") delay(20) job.cancel() println(received) // [услышано] } ``` ## replay `replay` задаёт, сколько последних событий получит новый подписчик: ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun main() = runBlocking { val events = MutableSharedFlow(replay = 2) events.emit(1) events.emit(2) events.emit(3) val received = mutableListOf() val job = launch { events.collect { received += it } } delay(20) job.cancel() println(received) // [2, 3] } ``` `StateFlow` эквивалентен `SharedFlow(replay = 1)` с дедупликацией и обязательным начальным значением. ## Буфер и стратегия переполнения ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.* fun main() = runBlocking { val dropping = MutableSharedFlow( replay = 0, extraBufferCapacity = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) val received = mutableListOf() val job = launch { dropping.collect { delay(50); received += it } } delay(20) repeat(10) { dropping.emit(it) } // emit не приостанавливается delay(200) job.cancel() println("часть событий отброшена: ${received.size < 10}") // часть событий отброшена: true } ``` | Параметр | Смысл | |---|---| | `replay` | сколько событий повторить новому подписчику | | `extraBufferCapacity` | буфер сверх `replay` | | `onBufferOverflow` | `SUSPEND` (по умолчанию), `DROP_OLDEST`, `DROP_LATEST` | Важно: `DROP_OLDEST`/`DROP_LATEST` работают только при ненулевом буфере (`replay + extraBufferCapacity > 0`). `tryEmit` - неприостанавливающаяся отправка: ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun main() = runBlocking { val noBuffer = MutableSharedFlow() println("без буфера и подписчиков: ${noBuffer.tryEmit(1)}") // без буфера и подписчиков: true val withSubscriber = MutableSharedFlow() val job = launch { withSubscriber.collect { delay(1000) } } delay(20) println("подписчик занят, буфера нет: ${withSubscriber.tryEmit(2)}") // подписчик занят, буфера нет: false job.cancel() } ``` Тонкость, которая удивляет: без подписчиков `tryEmit` возвращает `true`, хотя событие никто не получил - отправка «удалась», получателей просто не было. ## stateIn и shareIn Превращают холодный `Flow` в горячий: ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun main() = runBlocking { var starts = 0 val cold = flow { starts++ repeat(3) { delay(20); emit(it) } } val hot = cold.stateIn( scope = this, started = SharingStarted.Eagerly, initialValue = -1, ) delay(30) val a = launch { hot.take(1).collect { println("A видит: $it") } } val b = launch { hot.take(1).collect { println("B видит: $it") } } a.join(); b.join() println("источник запущен раз: $starts") // A видит: 0 // B видит: 0 // источник запущен раз: 1 } ``` Без `stateIn` два подписчика запустили бы источник дважды - два сетевых запроса вместо одного. `SharingStarted` определяет жизненный цикл источника: ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun main() = runBlocking { var active = 0 val source = flow { active++ try { repeat(100) { delay(10); emit(it) } } finally { active-- } } val shared = source.shareIn( scope = this, started = SharingStarted.WhileSubscribed(), replay = 1, ) val job = launch { shared.collect { } } delay(30) println("источник активен при подписчике: ${active == 1}") job.cancelAndJoin() delay(30) println("источник остановлен без подписчиков: ${active == 0}") coroutineContext.cancelChildren() // иначе runBlocking ждёт корутину shareIn // источник активен при подписчике: true // источник остановлен без подписчиков: true } ``` | `SharingStarted` | Когда источник активен | |---|---| | `Eagerly` | сразу и до отмены scope | | `Lazily` | с первого подписчика, затем навсегда | | `WhileSubscribed()` | только пока есть подписчики | | `WhileSubscribed(stopTimeoutMillis = 5000)` | плюс пауза перед остановкой | `WhileSubscribed(5000)` - стандартный выбор для Android: при повороте экрана подписчик пересоздаётся за миллисекунды, и таймаут не даёт зря перезапускать запрос. ## Как выбрать ```mermaid graph TD A["Нужен поток значений"] --> B{"Один подписчик,
работа по запросу?"} B -->|"да"| C["обычный Flow
холодный"] B -->|"нет, нужно делить"| D{"Есть текущее
значение?"} D -->|"да, это состояние"| E["StateFlow
initialValue обязателен"] D -->|"нет, это события"| F{"Нужна история
для новых подписчиков?"} F -->|"нет"| G["SharedFlow
replay = 0"] F -->|"да, n последних"| H["SharedFlow
replay = n"] ``` | | `StateFlow` | `SharedFlow` | |---|---|---| | начальное значение | обязательно | нет | | `.value` | есть | нет | | дедупликация | да, по `equals` | нет | | `replay` | всегда 1 | настраивается | | потеря промежуточных | да, конфляция | зависит от буфера | | типичное применение | состояние экрана, настройки | навигация, снекбары, ошибки | Практическое правило: состояние (что сейчас на экране) - `StateFlow`; события (что произошло один раз) - `SharedFlow`. Показ сообщения об ошибке через `StateFlow` приводит к повторному показу при пересоздании подписчика. ## Состояние экрана целиком ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* data class ScreenState( val loading: Boolean = false, val items: List = emptyList(), val error: String? = null, ) class ViewModel(private val scope: CoroutineScope) { private val _state = MutableStateFlow(ScreenState()) val state: StateFlow = _state.asStateFlow() fun load() { scope.launch { _state.update { it.copy(loading = true, error = null) } delay(20) _state.update { it.copy(loading = false, items = listOf("первый", "второй")) } } } } fun main() = runBlocking { val vm = ViewModel(this) val job = launch { vm.state.collect { println(it) } } delay(10) vm.load() delay(60) job.cancel() // ScreenState(loading=false, items=[], error=null) // ScreenState(loading=true, items=[], error=null) // ScreenState(loading=false, items=[первый, второй], error=null) } ``` Один `StateFlow` на всё состояние лучше трёх отдельных: подписчик видит согласованный снимок, а не промежуточные комбинации полей. `copy` + `update` делают изменение атомарным. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md) ## Производные состояния ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun main() = runBlocking { val query = MutableStateFlow("") val items = MutableStateFlow(listOf("кот", "кофе", "книга")) val filtered = combine(query, items) { q, list -> list.filter { it.startsWith(q) } }.stateIn(this, SharingStarted.Eagerly, emptyList()) delay(20) // Eagerly запускает корутину, но не мгновенно println(filtered.value) // [кот, кофе, книга] query.value = "ко" delay(20) println(filtered.value) // [кот, кофе] coroutineContext.cancelChildren() } ``` `combine` + `stateIn` - способ получить вычисляемое состояние из нескольких источников. Важно вызвать `cancelChildren()` или ограничить scope: `stateIn` с `Eagerly` запускает корутину, которая живёт до отмены области. ## subscriptionCount ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.flow.* fun main() = runBlocking { val events = MutableSharedFlow() println("подписчиков: ${events.subscriptionCount.value}") // подписчиков: 0 val job = launch { events.collect { } } delay(20) println("после подписки: ${events.subscriptionCount.value}") // после подписки: 1 job.cancelAndJoin() println("после отмены: ${events.subscriptionCount.value}") // после отмены: 0 } ``` Это `StateFlow`, поэтому за числом подписчиков можно наблюдать - именно так `WhileSubscribed` понимает, когда останавливать источник. > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | `Observable` + `Observer` (паттерн) | `StateFlow` | > | `PropertyChangeListener` | `StateFlow` | > | RxJava `BehaviorSubject` | `StateFlow` | > | RxJava `PublishSubject` | `SharedFlow(replay = 0)` | > | RxJava `ReplaySubject(n)` | `SharedFlow(replay = n)` | > | `AtomicReference` + слушатели | `MutableStateFlow` | > | `CopyOnWriteArrayList` | встроенная многоадресность | > > **Паттерн «наблюдатель» вручную.** В Java его пишут заново в каждом проекте: список слушателей, синхронизация при добавлении и удалении, защита от `ConcurrentModificationException` при оповещении, снятие слушателей при закрытии компонента. Классическая реализация: > ```java > private final List> listeners = new CopyOnWriteArrayList<>(); > private volatile int value; > > public void setValue(int v) { > this.value = v; > for (var l : listeners) l.accept(v); > } > public void addListener(Consumer l) { listeners.add(l); } > public void removeListener(Consumer l) { listeners.remove(l); } > ``` > Здесь уже есть проблемы: оповещение идёт в потоке вызывающего, медленный слушатель задерживает остальных, забытый `removeListener` - утечка. `MutableStateFlow` решает всё это за одну строку, а отписка происходит автоматически при отмене корутины-подписчика. > > **`BehaviorSubject` против `StateFlow`.** Прямой аналог, но с двумя отличиями. `BehaviorSubject` не дедуплицирует одинаковые значения - в Rx для этого добавляют `distinctUntilChanged()` руками. И `Subject` можно «завершить» через `onComplete`/`onError`, после чего он мёртв; `StateFlow` не завершается никогда - ошибки передают внутри состояния (поле `error`), что для UI удобнее: экран всегда в каком-то состоянии. > > **Утечки подписок.** В Rx подписка возвращает `Disposable`, который нужно хранить и вызывать `dispose()`; для борьбы с забывчивостью придумали `CompositeDisposable` и `AutoDispose`. В Kotlin подписчик - корутина, поэтому отмена scope отписывает всех: > ```kotlin > import kotlinx.coroutines.* > import kotlinx.coroutines.flow.* > > fun main() = runBlocking { > val state = MutableStateFlow(0) > val scope = CoroutineScope(Job()) > scope.launch { state.collect { } } > delay(20) > println("подписчиков: ${state.subscriptionCount.value}") > scope.cancel() > delay(20) > println("после cancel: ${state.subscriptionCount.value}") > // подписчиков: 1 > // после cancel: 0 > } > ``` > > **`volatile` поле против `StateFlow`.** `volatile int value` даёт видимость между потоками, но не даёт ни оповещения, ни атомарного обновления (`value++` всё равно гонка - нужен `AtomicInteger`). `MutableStateFlow` совмещает три роли: потокобезопасное хранилище, атомарный `update` и многоадресная рассылка. > > **Нет аналога `WhileSubscribed`.** В Java управление жизненным циклом источника при появлении и исчезновении слушателей пишут вручную: счётчик подписчиков, запуск потока при первом, остановка при последнем, таймаут на повторное подключение. `shareIn(started = WhileSubscribed(5000))` - это одна строка. > > Сравнить: [Многопоточность в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [Паттерны проектирования](../Функциональный%20Kotlin/Паттерны%20проектирования.md) > [!warning] Подводные камни > **`StateFlow` не доставит одинаковое значение дважды:** > ```kotlin > import kotlinx.coroutines.* > import kotlinx.coroutines.flow.* > > fun main() = runBlocking { > val showError = MutableStateFlow("ошибка сети") > var shown = 0 > val job = launch { showError.collect { shown++ } } > delay(20) > showError.value = "ошибка сети" // то же самое — не придёт > delay(20) > job.cancel() > println("показов: $shown") // показов: 1 > } > ``` > Для повторяющихся событий (две одинаковые ошибки подряд) нужен `SharedFlow`. > > **Изменяемое состояние внутри `StateFlow` ломает оповещение.** Если вы положили `MutableList` и меняете его содержимое, `equals` вернёт `true` и подписчики ничего не узнают. Храните неизменяемые `data class` и обновляйте через `copy`. > > **`state.value = state.value + 1` - гонка:** > ```kotlin > import kotlinx.coroutines.* > import kotlinx.coroutines.flow.* > > fun main() = runBlocking { > val state = MutableStateFlow(0) > withContext(Dispatchers.Default) { > List(50) { launch { repeat(200) { state.value = state.value + 1 } } } > .forEach { it.join() } > } > println("обновления потеряны: ${state.value < 10_000}") // обновления потеряны: true > } > ``` > Используйте `update { }`. > > **`SharedFlow` без подписчиков теряет события.** `emit` при `replay = 0` и нулевом буфере просто завершится, не доставив ничего. Для «событий, которые нельзя потерять» нужен `replay ≥ 1` или `Channel`. Подробнее: [Channels](Channels.md) > > **`emit` в `SharedFlow` с `SUSPEND` приостанавливается на медленном подписчике:** > ```kotlin > import kotlinx.coroutines.* > import kotlinx.coroutines.flow.* > import kotlin.system.measureTimeMillis > > fun main() = runBlocking { > val events = MutableSharedFlow() > val job = launch { events.collect { delay(100) } } > delay(20) > val t = measureTimeMillis { events.emit(1); events.emit(2) } > println("emit ждал подписчика: ${t >= 90}") // emit ждал подписчика: true > job.cancel() > } > ``` > Один зависший подписчик блокирует отправку всем. Задавайте `extraBufferCapacity` и стратегию переполнения. > > **`tryEmit` возвращает `true` без подписчиков**, хотя событие никто не получил. Проверка `if (tryEmit(x))` не означает «доставлено». > > **`DROP_OLDEST` без буфера не работает.** При `replay = 0` и `extraBufferCapacity = 0` стратегия переполнения игнорируется, потому что буфера нет - `emit` будет приостанавливаться. > > **`SharingStarted.Eagerly` держит источник навсегда:** > ```kotlin > import kotlinx.coroutines.* > import kotlinx.coroutines.flow.* > > fun main() = runBlocking { > var active = 0 > val source = flow { > active++ > try { repeat(1000) { delay(10); emit(it) } } finally { active-- } > } > val hot = source.stateIn(this, SharingStarted.Eagerly, -1) > delay(30) > println("работает без единого подписчика: ${active == 1}") > coroutineContext.cancelChildren() > // работает без единого подписчика: true > } > ``` > Если источник - опрос сервера, он будет работать и жечь батарею, пока жив scope. Для подписок UI нужен `WhileSubscribed()`. > > **`stateIn`/`shareIn` требуют scope, который кто-то отменит.** Передача `GlobalScope` или scope, живущего весь процесс, превращает их в утечку. > > **`StateFlow.value` - снимок, а не транзакция:** > ```kotlin > import kotlinx.coroutines.* > import kotlinx.coroutines.flow.* > > fun main() = runBlocking { > val a = MutableStateFlow(1) > val b = MutableStateFlow(10) > // между чтениями значения могли измениться > val sum = a.value + b.value > println("несогласованный снимок возможен: $sum") // несогласованный снимок возможен: 11 > } > ``` > Два связанных значения нужно держать в одном `StateFlow` как единый объект состояния. > > **`collect` на `StateFlow` никогда не завершается.** Это бесконечный поток: `toList()` на нём повиснет, `onCompletion` не вызовется сам. Нужен `take(n)`, `first()` или отмена корутины. > > **Подписчик, созданный до `emit`, может его пропустить:** > ```kotlin > import kotlinx.coroutines.* > import kotlinx.coroutines.flow.* > > fun main() = runBlocking { > val events = MutableSharedFlow() > launch { events.collect { println("не увидит: $it") } } > events.emit(1) // launch ещё не успел подписаться > delay(20) > coroutineContext.cancelChildren() > // (ничего не напечатано) > } > ``` > `launch` возвращается до начала тела, поэтому подписка ещё не оформлена. В тестах для этого используют `events.subscriptionCount.first { it > 0 }`, в продакшене - `replay ≥ 1`. > > **`asStateFlow()` не копирует состояние** - это обёртка только для чтения над тем же объектом. Если внешний код получит доступ к исходному `MutableStateFlow`, он сможет его менять. > > **Ошибка в подписчике отменяет его подписку, а не поток.** Исключение в теле `collect` уходит в корутину-подписчик и может уронить её scope, при этом `StateFlow` продолжит жить. Оборачивайте тело `collect` в `try/catch`, если ошибки ожидаемы. Далее: [Channels](Channels.md)