Flow и каналы
Flow — это холодный асинхронный поток: описание того, откуда берутся значения, а не сами значения. Билдер flow { ... } не делает ничего, пока к нему не придёт терминальный оператор collect; каждый новый коллектор запускает производителя заново, со своим независимым прогоном. Это отличает Flow от горячих потоков — StateFlow и SharedFlow, — которые живут и испускают значения независимо от наличия слушателей, и от Channel, горячей очереди для передачи значений между корутинами, где каждый элемент достаётся ровно одному получателю. Обратное давление здесь не отдельная машинерия, а прямое следствие того, что emit и collect — suspend-функции: медленный коллектор просто приостанавливает производителя.
Главный водораздел темы — холодное против горячего, и почти все ловушки живут ровно на нём. Холодный Flow не кэширует значений и не делится прогоном между коллекторами (два коллектора — два запуска). Промежуточные операторы (map, filter, catch) ленивы: они лишь строят цепочку и выполняются внутри корутины коллектора, поэтому flowOn меняет контекст только для upstream — того, что объявлено выше него, — и не может увести сам collect с вызывающего диспетчера. Горячий StateFlow всегда хранит одно текущее значение, склеивает быстрые эмиссии и отбрасывает значение, равное предыдущему (equals) — это состояние, а не события; для одноразовых событий берут SharedFlow, который ничего не дедуплицирует. А Channel нужен там, где значение обязано достаться одному потребителю, а не всем сразу. Разбор по слоям — ниже.
Карта темы
- Холодный Flow — почему производитель не запускается до
collectи перезапускается заново для каждого коллектора, и чем это отличается от одного значенияsuspend-функции. - Операторы Flow — промежуточные операторы строят ленивую цепочку, терминальные её запускают, а
catchловит только то, что выше по потоку. - flowOn и сохранение контекста — почему
flowOnвлияет только на upstream, не трогает коллектор и заменяет запрещённыйwithContextвнутриflow { }. - Обратное давление — приостановка
emitкак встроенный механизм и какbuffer,conflateиcollectLatestего меняют. - StateFlow — горячий держатель одного значения, склейка, отбрасывание дубликатов через
equalsи превращение холодного потока в состояние черезstateIn. - SharedFlow — настраиваемый горячий broadcast с
replayи буфером, почемуStateFlow— его частный случай и зачем брать именно его для событий. - Channel — горячая очередь с приостановкой, доставка каждого элемента одному получателю и обязанность закрыть или потребить, иначе утечка.
- Flow против Channel — холодный конвейер против горячей передачи, правило «сначала Flow» и роль
callbackFlowкак моста.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Думать, что холодный Flow кэширует значения и переигрывает их поздним коллекторам | Каждый коллектор запускает производителя заново — два коллектора дают два независимых прогона |
Ожидать, что холодный Flow начнёт производить значения до первого collect | До терминального оператора не выполняется ничего: нет коллектора — нет работы и нет побочных эффектов |
| Считать, что цепочка операторов делает работу до запуска терминального оператора | map/filter лишь возвращают новый Flow; вся работа идёт при collect, по разу на сбор |
| Думать, что каждый оператор получает собственную корутину или поток | Цепочка исполняется последовательно в одной корутине коллектора, без лишних потоков |
Ожидать, что catch обработает исключение из блока collect | catch ловит только upstream; ошибка коллектора — downstream и улетает вызывающему |
Считать, что flowOn уводит collect с вызывающего диспетчера | flowOn меняет контекст только для upstream; коллектор всегда в контексте вызвавшей корутины |
Ставить flowOn в конце цепочки, чтобы «покрыть» коллектор | flowOn действует вверх от места вызова; операторы и collect ниже него не затрагиваются |
Вызывать withContext(IO) { emit(x) } внутри flow { } | Нарушение сохранения контекста — рантайм-ошибка «Flow invariant is violated»; нужен .flowOn(IO) |
Считать, что Flow буферизует по умолчанию | По умолчанию сбор последователен и emit приостанавливается на медленном коллекторе — это и есть обратное давление |
Путать conflate и collectLatest | conflate отбрасывает промежуточные значения; collectLatest отменяет и перезапускает тело коллектора |
Использовать StateFlow для одноразовых событий | Он склеивает и отбрасывает значение, равное текущему по equals — два одинаковых события превращаются в одно |
Ожидать, что StateFlow доставит каждую эмиссию быстрого производителя | Он conflated: медленный коллектор увидит лишь последнее значение, промежуточные пропадут |
Думать, что SharedFlow требует начального значения, как StateFlow | SharedFlow не хранит значения; replay = 0 означает, что поздний подписчик пропустит прошлые события |
Ожидать, что Channel разошлёт копию каждого элемента всем получателям | Каждый элемент достаётся ровно одному получателю — это fan-out нагрузки, а не broadcast |
Думать, что Channel холодный и запускает свежего производителя на получателя | Channel горячий: несобираемый канал держит живыми производителя и буфер — это утечка |
Отдавать наружу горячий Channel там, где хватило бы холодного Flow | Уходит структурная безопасность холодного потока; для UI-состояния нужен StateFlow, а не raw Channel |
Значение для собеседований
Flow — это раздел, где кандидата проверяют на модель исполнения, а не на знание имён операторов. Ключевой вопрос темы — «холодный он или горячий и что это значит физически»: холодный Flow ничего не делает до collect и перезапускается на каждого коллектора, горячий StateFlow/SharedFlow живёт сам по себе. Из этого одного различия выводятся и ленивость операторов, и то, почему flowOn не трогает коллектор, и почему StateFlow теряет повтор. Интервьюер обычно ведёт от «чем Flow лучше suspend-функции» к «почему UI подвисает несмотря на flowOn» и «почему второй одинаковый toast не доходит» — три ловушки на одну и ту же модель.
Что обычно проверяют:
- Что значит «холодный»: когда запускается производитель и что видят два коллектора одного
Flow. - Когда реально выполняются промежуточные операторы и какие операторы терминальные.
- Что именно ловит
catchи почему ошибку внутриcollectон пропускает. - Направление
flowOn: почему он влияет только на upstream и не может увестиcollect. - Как
buffer,conflateиcollectLatestменяют поведение при быстром производителе. - Разницу
StateFlowиSharedFlowи почемуStateFlow— неверный носитель для событий. - Как
Channelдоставляет элементы двум получателям и когда братьChannelвместоFlow.
Типичный неверный ответ: «холодный Flow вычисляет значения один раз и раздаёт их всем коллекторам, как кэш». Отсюда кандидат делает вывод, что два коллектора делят один прогон, что SharedFlow — это тот, кто перезапускает производителя, и что flowOn в конце цепочки покроет и collect. На деле всё наоборот: холодный Flow ничего не кэширует и запускает производителя заново для каждого коллектора; SharedFlow горячий и делит один поток эмиссий; а flowOn меняет контекст только для того, что объявлено выше него, и коллектор всегда остаётся на вызывающем диспетчере.