619 lines
29 KiB
Markdown
619 lines
29 KiB
Markdown
---
|
||
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)
|