Files
obsidian/Kotlin/Корутины/StateFlow и SharedFlow.md
T

619 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<Int> = _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<String>()
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<Int>)
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<Int>()
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<String>()
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<String>()
events.emit("в пустоту") // подписчиков нет
val received = mutableListOf<String>()
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<Int>(replay = 2)
events.emit(1)
events.emit(2)
events.emit(3)
val received = mutableListOf<Int>()
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<Int>(
replay = 0,
extraBufferCapacity = 2,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
val received = mutableListOf<Int>()
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<Int>()
println("без буфера и подписчиков: ${noBuffer.tryEmit(1)}") // без буфера и подписчиков: true
val withSubscriber = MutableSharedFlow<Int>()
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{"Один подписчик,<br/>работа по запросу?"}
B -->|"да"| C["обычный Flow<br/>холодный"]
B -->|"нет, нужно делить"| D{"Есть текущее<br/>значение?"}
D -->|"да, это состояние"| E["StateFlow<br/>initialValue обязателен"]
D -->|"нет, это события"| F{"Нужна история<br/>для новых подписчиков?"}
F -->|"нет"| G["SharedFlow<br/>replay = 0"]
F -->|"да, n последних"| H["SharedFlow<br/>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<String> = emptyList(),
val error: String? = null,
)
class ViewModel(private val scope: CoroutineScope) {
private val _state = MutableStateFlow(ScreenState())
val state: StateFlow<ScreenState> = _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<Int>()
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<Int>`, поэтому за числом подписчиков можно наблюдать - именно так `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<Listener>` | встроенная многоадресность |
>
> **Паттерн «наблюдатель» вручную.** В Java его пишут заново в каждом проекте: список слушателей, синхронизация при добавлении и удалении, защита от `ConcurrentModificationException` при оповещении, снятие слушателей при закрытии компонента. Классическая реализация:
> ```java
> private final List<Consumer<Integer>> 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<Integer> l) { listeners.add(l); }
> public void removeListener(Consumer<Integer> 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<Int>()
> 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<Int>()
> 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)