Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,618 @@
---
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)