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
+783
View File
@@ -0,0 +1,783 @@
---
title: Flow
description: Холодный асинхронный поток значений: построение, операторы, контекст, обработка ошибок и backpressure
tags:
- kotlin
- coroutines
- flow
---
`Flow<T>` - асинхронная последовательность значений. Если `suspend`-функция возвращает одно значение, то `Flow` - много, причём каждое может приходить с задержкой.
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun temperatures(): Flow<Int> = flow {
for (t in listOf(20, 22, 25)) {
delay(20) // внутри flow можно приостанавливаться
emit(t)
}
}
fun main() = runBlocking {
temperatures().collect { println("температура: $it") }
// температура: 20
// температура: 22
// температура: 25
}
```
Сравните с `List`: список нужно вычислить целиком, прежде чем вернуть. `Flow` отдаёт значения по мере готовности.
## Холодный поток
`Flow` ничего не делает, пока его не начнут собирать. Каждый вызов `collect` запускает тело заново.
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
var starts = 0
val f = flow {
starts++
emit(1)
emit(2)
}
println("создан, запусков: $starts") // создан, запусков: 0
f.collect { }
f.collect { }
println("после двух collect: $starts") // после двух collect: 2
}
```
Это главное свойство: `Flow` - **описание** вычисления, а не запущенный процесс. Горячие потоки, которые работают независимо от подписчиков, - это `StateFlow` и `SharedFlow`. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md)
## Способы создать Flow
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
flowOf(1, 2, 3).collect { print("$it ") }
println() // 1 2 3
listOf("a", "b").asFlow().collect { print("$it ") }
println() // a b
(1..3).asFlow().collect { print("$it ") }
println() // 1 2 3
flow {
emit("из билдера")
emitAll(flowOf("и", "вложенного"))
}.collect { print("$it ") }
println() // из билдера и вложенного
}
```
Для колбэчных источников есть `callbackFlow` - он же мост от слушателей к `Flow`:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.*
class Sensor {
private var listener: ((Int) -> Unit)? = null
fun subscribe(l: (Int) -> Unit) { listener = l }
fun unsubscribe() { listener = null; println("слушатель снят") }
fun fire(value: Int) = listener?.invoke(value)
}
fun Sensor.readings(): Flow<Int> = callbackFlow {
subscribe { value -> trySend(value) }
awaitClose { unsubscribe() } // вызовется при отмене collect
}
fun main() = runBlocking {
val sensor = Sensor()
val job = launch {
sensor.readings().collect { println("показание: $it") }
}
delay(20)
sensor.fire(42)
sensor.fire(43)
delay(20)
job.cancelAndJoin()
// показание: 42
// показание: 43
// слушатель снят
}
```
`awaitClose` обязателен: без него `callbackFlow` бросит `IllegalStateException`, потому что иначе слушатель остался бы висеть.
## Промежуточные операторы
Операторы ленивые: они только оборачивают поток, не запуская его.
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
val result = (1..10).asFlow()
.filter { it % 2 == 0 }
.map { it * it }
.take(3)
.toList()
println(result) // [4, 16, 36]
}
```
Главное отличие от операторов коллекций - внутри можно приостанавливаться:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
suspend fun enrich(id: Int): String { delay(10); return "запись $id" }
fun main() = runBlocking {
flowOf(1, 2, 3)
.map { enrich(it) } // suspend внутри map
.collect { println(it) }
// запись 1
// запись 2
// запись 3
}
```
| Оператор | Что делает |
|---|---|
| `map`, `filter` | как у коллекций, но с `suspend` внутри |
| `transform` | произвольное число `emit` на элемент |
| `take`, `drop`, `takeWhile` | ограничение по количеству |
| `onEach` | побочный эффект без изменения значений |
| `withIndex`, `runningFold`, `scan` | накопление |
| `distinctUntilChanged` | пропуск подряд идущих дублей |
| `debounce`, `sample` | фильтрация по времени |
| `flatMapConcat`, `flatMapMerge`, `flatMapLatest` | поток из потоков |
| `zip`, `combine` | объединение двух потоков |
| `onStart`, `onCompletion`, `catch` | обработка жизненного цикла |
`transform` - самый общий:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
flowOf(1, 2, 3)
.transform { value ->
emit("начало $value")
emit("конец $value")
}
.collect { print("$it; ") }
println()
// начало 1; конец 1; начало 2; конец 2; начало 3; конец 3;
}
```
## Терминальные операторы
Только они запускают поток, и все они `suspend`:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
val f = flowOf(1, 2, 3, 4)
println(f.toList()) // [1, 2, 3, 4]
println(f.first()) // 1
println(f.firstOrNull { it > 10 }) // null
println(f.lastOrNull()) // 4
println(f.count()) // 4
println(f.reduce { a, b -> a + b }) // 10
println(f.fold(100) { a, b -> a + b }) // 110
println(f.toSet()) // [1, 2, 3, 4]
println(f.filter { it == 2 }.single()) // 2
}
```
`first()` отменяет поток сразу после первого значения - этого достаточно, чтобы остановить бесконечный источник:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
var emitted = 0
val infinite = flow {
var i = 0
while (true) {
emitted++
emit(i++)
}
}
println(infinite.first()) // 0
println("излучено значений: $emitted") // излучено значений: 1
}
```
## Контекст и flowOn
По умолчанию `Flow` выполняется в контексте того, кто его собирает - это называется context preservation:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
flow {
println("flow на: ${Thread.currentThread().name}")
emit(1)
}.collect {
println("collect на: ${Thread.currentThread().name}")
}
// flow на: main
// collect на: main
}
```
Сменить контекст **производителя** можно только через `flowOn`:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
flow {
val onPool = Thread.currentThread().name.contains("DefaultDispatcher")
println("flow в пуле: $onPool")
emit(1)
}
.flowOn(Dispatchers.IO)
.collect {
println("collect на: ${Thread.currentThread().name}")
}
// flow в пуле: true
// collect на: main
}
```
`flowOn` действует на всё, что **выше** него по цепочке:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
flowOf(1, 2)
.map { it * 10 } // в IO
.flowOn(Dispatchers.IO)
.map { it + 1 } // в main
.collect { print("$it ") }
println() // 11 21
}
```
Менять контекст внутри `flow { }` через `withContext` запрещено:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
val broken = flow {
withContext(Dispatchers.IO) { emit(1) } // нарушение контракта
}
val error = runCatching { broken.collect { } }.exceptionOrNull()
println(error is IllegalStateException) // true
println(error?.message?.contains("Flow invariant is violated")) // true
}
```
Причина запрета - предсказуемость: собирающий код должен быть уверен, что `collect` вызывается в его контексте (например, в UI-потоке). Для смены контекста есть только `flowOn`.
## Обработка ошибок
`try/catch` работает, но правильнее оператор `catch`:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
flow {
emit(1)
throw IllegalStateException("источник упал")
}
.catch { e -> println("перехвачено: ${e.message}") }
.collect { println("значение: $it") }
// значение: 1
// перехвачено: источник упал
}
```
`catch` ловит ошибки только **выше** себя - это называется прозрачность исключений:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
val error = runCatching {
flowOf(1)
.catch { println("не сработает") }
.collect { throw IllegalStateException("ошибка в collect") }
}.exceptionOrNull()
println("ошибка collect прошла мимо catch: ${error?.message}")
// ошибка collect прошла мимо catch: ошибка в collect
}
```
Чтобы ловить и ошибки потребителя, нужен `onEach` + `catch` в таком порядке:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
flowOf(1, 2)
.onEach { if (it == 2) error("обработка упала на $it") }
.catch { e -> println("перехвачено: ${e.message}") }
.collect { println("обработано: $it") }
// обработано: 1
// перехвачено: обработка упала на 2
}
```
`catch` может подменить поток значениями по умолчанию:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
flow<String> { error("сеть недоступна") }
.catch { emit("данные из кеша") }
.collect { println(it) }
// данные из кеша
}
```
## retry и повторные попытки
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
var attempt = 0
flow {
attempt++
if (attempt < 3) error("попытка $attempt не удалась")
emit("успех на попытке $attempt")
}
.retry(3) { e ->
println("ретрай после: ${e.message}")
delay(10)
true
}
.collect { println(it) }
// ретрай после: попытка 1 не удалась
// ретрай после: попытка 2 не удалась
// успех на попытке 3
}
```
`retryWhen` даёт доступ к номеру попытки - это нужно для экспоненциальной задержки:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
var calls = 0
flow {
calls++
if (calls < 3) error("сбой")
emit("готово")
}
.retryWhen { _, attempt ->
if (attempt >= 3) false
else {
delay(10L * (attempt + 1)) // 10, 20, 30 мс
println("попытка ${attempt + 1}")
true
}
}
.collect { println(it) }
// попытка 1
// попытка 2
// готово
}
```
## Жизненный цикл: onStart, onCompletion
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
flowOf(1, 2)
.onStart { println("начали") }
.onEach { println("значение $it") }
.onCompletion { cause -> println("завершили, ошибка: $cause") }
.collect { }
// начали
// значение 1
// значение 2
// завершили, ошибка: null
}
```
`onCompletion` вызывается всегда - при успехе, ошибке и отмене, причём получает причину:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
flow {
emit(1)
error("упало")
}
.onCompletion { cause -> println("причина: ${cause?.message}") }
.catch { }
.collect { }
// причина: упало
}
```
`onStart` может излучать значения - это способ показать «загрузка...» до первого реального элемента:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
flow { delay(20); emit("данные") }
.onStart { emit("загрузка...") }
.collect { println(it) }
// загрузка...
// данные
}
```
## buffer: производитель и потребитель параллельно
По умолчанию производитель ждёт потребителя:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.measureTimeMillis
fun source() = flow {
repeat(3) {
delay(20) // производство
emit(it)
}
}
fun main() = runBlocking {
val sequential = measureTimeMillis {
source().collect { delay(20) } // потребление
}
val buffered = measureTimeMillis {
source().buffer().collect { delay(20) }
}
println("без буфера около 120 мс: ${sequential >= 110}")
println("с буфером быстрее: ${buffered < sequential}")
// без буфера около 120 мс: true
// с буфером быстрее: true
}
```
Без буфера шаги складываются: 3 × (20 + 20) = 120 мс. С буфером производство идёт параллельно с потреблением, и общее время приближается к максимуму из двух конвейеров.
## conflate и collectLatest
Когда важны только свежие значения, промежуточные можно отбрасывать:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun fast() = flow {
repeat(5) {
delay(10)
emit(it)
}
}
fun main() = runBlocking {
val all = mutableListOf<Int>()
fast().collect { delay(40); all += it }
val conflated = mutableListOf<Int>()
fast().conflate().collect { delay(40); conflated += it }
println("без conflate получено всё: ${all.size == 5}")
println("с conflate пропущены промежуточные: ${conflated.size < 5}")
println("последнее значение всегда доходит: ${conflated.last() == 4}")
// без conflate получено всё: true
// с conflate пропущены промежуточные: true
// последнее значение всегда доходит: true
}
```
`collectLatest` работает иначе: он не пропускает значения, а **отменяет** обработку предыдущего:
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
val completed = mutableListOf<Int>()
flow {
repeat(3) { delay(10); emit(it) }
}.collectLatest { value ->
delay(25) // не успеет для первых двух
completed += value
}
println("доработал только последний: $completed") // доработал только последний: [2]
}
```
| Стратегия | Поведение при быстром производителе |
|---|---|
| по умолчанию | производитель ждёт потребителя |
| `buffer(n)` | очередь на `n` элементов, затем ожидание |
| `buffer(onBufferOverflow = DROP_OLDEST)` | выбрасывает старые |
| `conflate()` | хранит только последнее непрочитанное |
| `collectLatest` | отменяет обработку устаревшего значения |
| `debounce(ms)` | пропускает только после паузы |
| `sample(ms)` | берёт последнее раз в интервал |
## Объединение потоков
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
val letters = flowOf("a", "b", "c")
val numbers = flowOf(1, 2, 3)
println(letters.zip(numbers) { l, n -> "$l$n" }.toList()) // [a1, b2, c3]
val slow = flow { emit(1); delay(30); emit(2) }
val fast2 = flow { emit("x"); delay(10); emit("y") }
println(slow.combine(fast2) { a, b -> "$a$b" }.toList())
// [1x, 1y, 2y]
}
```
`zip` ждёт пару - элементов будет столько, сколько в коротком потоке. `combine` излучает при каждом изменении любого из потоков, используя последние известные значения другого. Для UI (например, «форма валидна, если валидны все поля») нужен `combine`.
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
val concatenated = flowOf(1, 2).flatMapConcat { n ->
flow { emit("$n-a"); delay(10); emit("$n-b") }
}.toList()
println(concatenated) // [1-a, 1-b, 2-a, 2-b]
val latest = flowOf(1, 2, 3).flatMapLatest { n ->
flow { delay(20); emit("результат $n") }
}.toList()
println(latest) // [результат 3]
}
```
`flatMapLatest` - основной оператор для поиска по вводу: новый запрос отменяет предыдущий.
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
suspend fun search(query: String): List<String> {
delay(20)
return listOf("результат для «$query»")
}
fun main() = runBlocking {
val input = flow {
emit("к"); delay(5)
emit("ко"); delay(5)
emit("кот"); delay(100) // пользователь остановился
emit("кота")
}
input
.debounce(30) // ждём паузу в наборе
.filter { it.length >= 2 }
.flatMapLatest { search(it).asFlow() }
.collect { println(it) }
// результат для «кот»
// результат для «кота»
}
```
Это каноничный конвейер: `debounce` отсекает промежуточные нажатия, `flatMapLatest` отменяет устаревший запрос.
```mermaid
graph LR
A["ввод: к, ко, кот"] --> B["debounce(30)"]
B --> C["только 'кот'<br/>после паузы"]
C --> D["flatMapLatest"]
D --> E["запрос к API"]
E --> F["collect: результаты"]
D -.->|"пришёл новый ввод"| G["отмена<br/>предыдущего запроса"]
```
## Flow как возвращаемый тип
```kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
class OrderRepository {
// не suspend: Flow холодный, вызов ничего не запускает
fun observeOrders(): Flow<String> = flow {
repeat(3) { i ->
delay(10)
emit("заказ $i")
}
}.flowOn(Dispatchers.IO) // производитель всегда в IO
}
fun main() = runBlocking {
OrderRepository().observeOrders().collect { println(it) }
// заказ 0
// заказ 1
// заказ 2
}
```
Правила проектирования: функция, возвращающая `Flow`, не должна быть `suspend` (создание потока мгновенно), а `flowOn` ставится внутри - вызывающий не обязан знать, где работает источник.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `Stream<T>` (синхронный, одноразовый) | `Sequence<T>` |
> | `Flow` из Reactive Streams | `Flow<T>` |
> | RxJava `Observable`/`Flowable` | `Flow<T>` |
> | Project Reactor `Flux<T>` | `Flow<T>` |
> | `Subscriber.request(n)` | приостановка `emit` |
> | `subscribeOn`/`observeOn` | `flowOn` |
> | `onErrorResume` | `catch` |
> | `BlockingQueue` + поток | `Channel` |
>
> **`java.util.stream.Stream` - это не аналог.** `Stream` синхронный: `stream.map(f)` не может ждать ответа сети, а терминальная операция блокирует поток до конца обработки. Прямой аналог `Stream` в Kotlin - `Sequence`. `Flow` отличается тем, что внутри каждого оператора разрешена приостановка. Подробнее: [Sequence](../Коллекции/Sequence.md)
>
> **Reactive Streams решают ту же задачу, но сложнее.** RxJava и Reactor построены на коллбэках (`onNext`, `onError`, `onComplete`) и требуют явного управления давлением через `request(n)`. В `Flow` backpressure получается бесплатно: `emit` - это `suspend`-функция, поэтому если потребитель не готов, производитель просто приостанавливается:
> ```kotlin
> import kotlinx.coroutines.*
> import kotlinx.coroutines.flow.*
>
> fun main() = runBlocking {
> var produced = 0
> flow {
> repeat(100) { produced++; emit(it) }
> }.take(3).collect { }
> println("произведено ровно столько, сколько взяли: $produced")
> // произведено ровно столько, сколько взяли: 3
> }
> ```
> Никакого `request(n)` - producer сам останавливается на `emit`.
>
> **Стектрейсы и отладка.** В RxJava стектрейс ошибки состоит из кадров библиотеки, а место в бизнес-логике приходится искать по `onAssembly`-хукам. `Flow` - обычные `suspend`-функции, поэтому исключение приходит с кадрами вашего кода, а `try/catch`, `for` и `return` работают внутри операторов естественно.
>
> **Количество операторов.** В RxJava их несколько сотен (`flatMapIterable`, `concatMapDelayError`, `switchIfEmpty`...), потому что каждый сценарий требует отдельного оператора - внутри цепочки нельзя писать обычный код. В `Flow` операторов несколько десятков: остальное выражается через `transform` и обычные конструкции языка.
>
> **Отмена подписки.** В Rx нужно хранить `Disposable` и не забыть вызвать `dispose()` - классический источник утечек, для борьбы с которым придумали `CompositeDisposable`. `Flow` собирается внутри корутины, поэтому отмена области автоматически прекращает сбор:
> ```kotlin
> import kotlinx.coroutines.*
> import kotlinx.coroutines.flow.*
>
> fun main() = runBlocking {
> val job = launch {
> flow {
> try {
> repeat(100) { delay(10); emit(it) }
> } finally { println("источник закрыт") }
> }.collect { }
> }
> delay(30)
> job.cancelAndJoin()
> // источник закрыт
> }
> ```
>
> **Context preservation - то, чего нет в Rx.** В RxJava `observeOn` можно поставить в любом месте, и понять, на каком потоке выполнится `subscribe`, можно только прочитав всю цепочку. `Flow` гарантирует, что `collect` всегда выполняется в контексте собирающего, а `withContext` внутри `flow { }` запрещён на уровне рантайма.
>
> Сравнить: [Многопоточность в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md)
> [!warning] Подводные камни
> **`Flow` не запустится без терминального оператора:**
> ```kotlin
> import kotlinx.coroutines.*
> import kotlinx.coroutines.flow.*
>
> fun main() = runBlocking {
> var side = 0
> flowOf(1, 2, 3).onEach { side++ } // collect не вызван
> delay(20)
> println("побочных эффектов: $side") // побочных эффектов: 0
> }
> ```
> Компилятор предупреждает о неиспользованном результате, но не всегда - например, если значение присвоено переменной.
>
> **`withContext` внутри `flow { }` бросает исключение в рантайме:**
> ```kotlin
> import kotlinx.coroutines.*
> import kotlinx.coroutines.flow.*
>
> fun main() = runBlocking {
> val error = runCatching {
> flow { withContext(Dispatchers.IO) { emit(1) } }.collect { }
> }.exceptionOrNull()
> println(error?.message?.contains("Flow invariant is violated")) // true
> }
> ```
> Это ошибка времени выполнения, а не компиляции - её легко пропустить в редкой ветке кода. Используйте `flowOn`.
>
> **`flowOn` действует только выше по цепочке.** `flow.map { a }.flowOn(IO).map { b }` выполнит `a` в IO, а `b` - в контексте сборщика. Порядок операторов меняет смысл.
>
> **`catch` не ловит ошибки `collect`:**
> ```kotlin
> import kotlinx.coroutines.*
> import kotlinx.coroutines.flow.*
>
> fun main() = runBlocking {
> val error = runCatching {
> flowOf(1).catch { println("не вызовется") }.collect { error("из collect") }
> }.exceptionOrNull()
> println("ошибка прошла мимо: ${error?.message}") // ошибка прошла мимо: из collect
> }
> ```
> Перенесите логику в `onEach` перед `catch`.
>
> **`emit` из другой корутины запрещён:**
> ```kotlin
> import kotlinx.coroutines.*
> import kotlinx.coroutines.flow.*
>
> fun main() = runBlocking {
> val broken = flow {
> coroutineScope {
> launch { emit(1) } // emit в другой корутине
> }
> }
> val error = runCatching { broken.collect { } }.exceptionOrNull()
> println(error != null) // true
> }
> ```
> Сообщение - про нарушение инварианта. Для излучения из нескольких источников нужен `channelFlow` или `callbackFlow`.
>
> **`callbackFlow` без `awaitClose` падает:**
> ```kotlin
> import kotlinx.coroutines.*
> import kotlinx.coroutines.flow.*
>
> fun main() = runBlocking {
> val broken = callbackFlow<Int> {
> trySend(1)
> // нет awaitClose
> }
> val error = runCatching { broken.collect { } }.exceptionOrNull()
> println(error is IllegalStateException) // true
> }
> ```
> Это защита от утечки слушателя: без `awaitClose` библиотека не знает, когда отписываться.
>
> **`trySend` молча теряет значения при полном буфере.** У `callbackFlow` буфер по умолчанию 64 элемента; `trySend` возвращает `ChannelResult`, который обычно игнорируют. Если источник быстрее потребителя, часть событий исчезнет - проверяйте результат или задавайте `buffer(Channel.UNLIMITED)` осознанно.
>
> **Повторный `collect` запускает поток заново.** Если `Flow` делает сетевой запрос, два `collect` - это два запроса. Для разделения результата между подписчиками нужен `shareIn`/`stateIn`. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md)
>
> **`Flow` без буфера последователен, а не параллелен:**
> ```kotlin
> import kotlinx.coroutines.*
> import kotlinx.coroutines.flow.*
> import kotlin.system.measureTimeMillis
>
> fun main() = runBlocking {
> val t = measureTimeMillis {
> flowOf(1, 2, 3).map { delay(30); it }.collect { }
> }
> println("последовательно, около 90 мс: ${t >= 85}") // последовательно, около 90 мс: true
> }
> ```
> `map` с `suspend` внутри обрабатывает элементы по одному. Для параллельной обработки нужен `flatMapMerge` с `concurrency` или `map { async { } }` поверх списка.
>
> **`conflate` и `collectLatest` теряют данные - это их цель.** Для UI-состояния это правильно, для обработки платежей или записи в журнал - нет. Убедитесь, что потеря промежуточных значений допустима.
>
> **`debounce` задерживает даже последнее значение.** Пользователь перестал печатать - результат появится только через указанный интервал. Слишком большое значение ощущается как лаг интерфейса.
>
> **Бесконечный `Flow` в `toList()` повиснет.** Терминальные операторы, собирающие всё (`toList`, `count`, `fold`), на бесконечном источнике не завершатся никогда. Нужен `take(n)` или `first()`.
>
> **`SharingStarted.Eagerly` в `stateIn` держит источник активным всегда** - типичная утечка при переносе холодного `Flow` в горячий. Подробнее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md)
>
> **Исключение в `onCompletion` не перехватывается предыдущим `catch`.** `onCompletion` - не `finally`: он не может подавить ошибку, а своя ошибка внутри него уйдёт потребителю.
>
> **`first()` отменяет источник.** Для `callbackFlow` это значит, что слушатель будет снят сразу после первого значения - если вы ожидали продолжения подписки, её нет.
Далее: [StateFlow и SharedFlow](StateFlow%20и%20SharedFlow.md)