Flow и каналы
Асинхронные потоки в kotlinx.coroutines — холодный Flow против горячих SharedFlow и StateFlow, операторы, обратное давление через buffer, conflate и collectLatest, почему flowOn влияет только на upstream, и когда Channel лучше Flow.
11 вопросов
JuniorТеорияОчень частоЧем холодный Flow отличается от горячего SharedFlow?
Чем холодный Flow отличается от горячего SharedFlow?
Холодный Flow не делает ничего, пока его не собирают, и перезапускает производителя отдельно для каждого коллектора, поэтому каждый получает свою личную последовательность. SharedFlow горячий: он испускает значения независимо от наличия слушателей, и все его коллекторы делят один поток эмиссий от одного производителя.
Типичные ошибки
- ✗Думать, что холодный
Flowкэширует значения и переигрывает их поздним коллекторам - ✗Ожидать, что
SharedFlowперестанет испускать значения, когда его никто не собирает - ✗Считать, что два коллектора холодного
Flowделят один прогон производителя
Уточняющие вопросы
- →Как превратить холодный
Flowв горячий, разделяемый всеми коллекторами? - →Что
SharedFlowделает со значениями, испущенными до первого подписчика?
JuniorТеорияОчень частоКогда промежуточные операторы вроде map и filter реально выполняются на Flow?
Когда промежуточные операторы вроде map и filter реально выполняются на Flow?
Никогда в момент объявления. Промежуточный оператор вроде map, filter или onEach лишь возвращает новый Flow, оборачивающий предыдущий, так что построение цепочки не делает никакой работы. Вся цепочка выполняется, когда её запускает терминальный оператор — collect, first, toList, — по разу на каждый сбор.
Типичные ошибки
- ✗Считать, что цепочка операторов делает работу до запуска терминального оператора
- ✗Думать, что каждый оператор получает собственную корутину или поток
- ✗Ожидать, что
collectпереиспользует результаты предыдущего сбора
Уточняющие вопросы
- →Какие операторы терминальные и что каждый из них делает с цепочкой?
- →Чем
onEachотличается отcollect, если оба смотрят на каждое значение?
JuniorТеорияЧастоЧто даёт Flow такого, чего не может suspend-функция с одним значением?
Что даёт Flow такого, чего не может suspend-функция с одним значением?
suspend-функция выполняется один раз и возвращает одно значение. Flow испускает много значений во времени, и коллектор обрабатывает каждое по мере поступления. Flow холодный: блок-производитель запускается лишь при вызове терминального оператора вроде collect и начинается заново для каждого коллектора.
Типичные ошибки
- ✗Думать, что холодный
Flowпроизводит значения до того, как его начали собирать - ✗Считать, что производитель отрабатывает один раз и потом делится между коллекторами
- ✗Воспринимать
Flowкак заранее вычисленную коллекцию, а не поток значений во времени
Уточняющие вопросы
- →Что происходит, когда два коллектора собирают один и тот же холодный
Flow? - →Какие терминальные операторы, кроме
collect, запускаютFlow?
MiddleТеорияЧастоКак Channel доставляет элементы, когда из него принимают две корутины?
Как Channel доставляет элементы, когда из него принимают две корутины?
Channel — это горячая очередь с приостановкой: send приостанавливается, пока буфер полон, а receive — пока он пуст. Каждый элемент достаётся ровно ОДНОМУ получателю: два получателя разбирают работу между собой, а не получают по копии. Именно это делает его инструментом передачи producer/consumer.
Типичные ошибки
- ✗Ожидать, что
Channelразошлёт копию каждого элемента всем получателям - ✗Думать, что
Channelхолодный и запускает свежего производителя на получателя - ✗Забывать, что
sendприостанавливается на полном буфере, а не отбрасывает элементы
Уточняющие вопросы
- →Как ёмкости
RENDEZVOUS,BUFFEREDиCONFLATEDменяют поведениеsend? - →Что происходит с приостановленным
receive, когда канал закрывают или отменяют?
MiddleДебаггингЧастоПочему UI всё равно подвисает, хотя в репозитории добавили flowOn(Dispatchers.IO)?
Почему UI всё равно подвисает, хотя в репозитории добавили flowOn(Dispatchers.IO)?
flowOn меняет контекст только для операторов ВЫШЕ по цепочке — для производителя и всего, что объявлено до вызова. Коллектор всегда работает в контексте той корутины, которая вызвала collect, потому что Flow сохраняет контекст. Поэтому parseHeavy внутри collect остаётся на главном потоке; перенесите его выше flowOn.
Типичные ошибки
- ✗Считать, что
flowOnуводитcollectс вызывающего диспетчера - ✗Думать, что
flowOnдействует на операторы, стоящие после него в цепочке - ✗Делать тяжёлую работу CPU внутри блока collect на главном диспетчере
Уточняющие вопросы
- →Что означает сохранение контекста для
Flowи зачем оно навязано? - →Что сделают два вызова
flowOnв одной цепочке с операторами между ними?
MiddleПроизводительностьИногдаКак buffer, conflate и collectLatest ведут себя, когда производитель быстрее коллектора?
Как buffer, conflate и collectLatest ведут себя, когда производитель быстрее коллектора?
По умолчанию сбор последовательный: медленный коллектор создаёт обратное давление, и emit приостанавливается. buffer разводит производителя и коллектор по отдельным корутинам с очередью между ними. conflate оставляет лишь самое свежее значение, отбрасывая промежуточные. collectLatest отменяет тело коллектора на каждом новом значении и перезапускает его.
Типичные ошибки
- ✗Думать, что
conflateтормозит производителя, а не отбрасывает промежуточные значения - ✗Ожидать, что
collectLatestдождётся прошлого тела коллектора, а не отменит его - ✗Считать, что
Flowбуферизует по умолчанию, а не приостанавливаетemit
Уточняющие вопросы
- →Что меняет
buffer(onBufferOverflow = DROP_OLDEST)по сравнению с обычнымconflate? - →Почему тело под
collectLatestобязано быть отменяемым, чтобы оператор вообще помог?
MiddleДебаггингИногдаПочему второй одинаковый error-toast не доходит до UI через этот StateFlow?
Почему второй одинаковый error-toast не доходит до UI через этот StateFlow?
StateFlow склеивает эмиссии и ведёт себя так, будто на нём стоит distinctUntilChanged, поэтому присваивание значения, равного текущему, не испускает ничего — второй одинаковый Toast отбрасывается до коллектора. StateFlow моделирует состояние, а не события: публикуйте их через MutableSharedFlow и emit.
Типичные ошибки
- ✗Использовать
StateFlowдля событий и винить коллектор, когда дубликат пропадает - ✗Забывать, что
StateFlowотбрасывает значение, равное уже хранимому - ✗Латать потерю ручным флагом обработки вместо
SharedFlowдля событий
Уточняющие вопросы
- →Почему
MutableSharedFlowсreplay = 0доставляет два одинаковых события? - →Что сделает
extraBufferCapacity = 0сemit, когда коллектор медленный?
MiddleТеорияИногдаКогда поток данных стоит отдавать наружу как Flow, а не как Channel?
Когда поток данных стоит отдавать наружу как Flow, а не как Channel?
Отдавайте Flow, когда каждый коллектор должен видеть весь поток, а производитель — стартовать по требованию: он холодный и перезапускается на коллектора, так что ничего не течёт, если его никто не собирает. Channel берите только для горячей передачи, где элемент обязан достаться ровно одному потребителю.
Типичные ошибки
- ✗Отдавать из репозитория горячий
Channelтам, где хватило бы холодногоFlow - ✗Считать, что
Channelкопирует каждый элемент каждому получателю - ✗Забывать, что несобираемый
Channelдержит живыми производителя и буфер
Уточняющие вопросы
- →Чем
receiveAsFlowотличается отconsumeAsFlow, когда появляются два коллектора? - →Что из двух вы возьмёте, чтобы раздавать задания пулу воркеров, и почему?
SeniorДизайнРедкоРепозиторий должен отдавать observeArticles(): Flow<List<Article>> одному экрану. Экран обязан сразу показать закэшированные данные из локальной базы, затем обновиться из сети и показать новый список без переподписки. При сетевом сбое закэшированный список должен остаться на экране, а ошибка — прийти отдельно. Поворот устройства не должен вызывать второй сетевой запрос за теми же данными, а испускаемый список не должен отставать от базы. Опишите устройство: откуда берутся испускаемые данные, что запускает обновление, что несёт ошибку и как поток остаётся единственным источником истины.
Репозиторий должен отдавать observeArticles(): Flow<List<Article>> одному экрану. Экран обязан сразу показать закэшированные данные из локальной базы, затем обновиться из сети и показать новый список без переподписки. При сетевом сбое закэшированный список должен остаться на экране, а ошибка — прийти отдельно. Поворот устройства не должен вызывать второй сетевой запрос за теми же данными, а испускаемый список не должен отставать от базы. Опишите устройство: откуда берутся испускаемые данные, что запускает обновление, что несёт ошибку и как поток остаётся единственным источником истины.
Сделайте базу единственным источником истины: возвращайте её query-Flow, тогда любая запись переиспускает список и UI не отстаёт от хранилища. Обновление запускайте из самого потока, а сетевой результат пишите В базу, а не испускайте. Ошибки отдавайте отдельным потоком, чтобы неудачное обновление оставило кэш нетронутым. Разделяйте поток через stateIn(WhileSubscribed).
Типичные ошибки
- ✗Испускать сетевой результат напрямую вместо записи его в базу данных
- ✗Позволять сбою обновления обрушить поток и стереть закэшированный список
- ✗Пересобирать холодный поток на повороте и слать дублирующий сетевой запрос
Уточняющие вопросы
- →Почему
WhileSubscribedс малым таймаутом переживает поворот, но не реальный выход? - →Как сообщить экрану, что обновление в процессе, не трогая сам список?
SeniorТеорияРедкоЧто на самом деле ловит оператор catch и какой сбой он пропускает?
Что на самом деле ловит оператор catch и какой сбой он пропускает?
catch работает только вверх по цепочке: он видит исключения производителя и операторов, объявленных ДО него, и больше ничего. Исключение внутри collect находится ниже, поэтому catch его не увидит и оно улетит вызывающему — это и есть exception transparency. Для временных сбоев выше берите retry/retryWhen, а для ошибок коллектора — обычный try/catch.
Типичные ошибки
- ✗Ожидать, что
catchобработает исключение, брошенное внутри блокаcollect - ✗Ставить
catchдо того оператора, чей сбой он должен был обработать - ✗Использовать
catchтам, где сбой временный и нуженretry/retryWhen
Уточняющие вопросы
- →Почему
catchпослеmapвидит исключение изmap, аcatchдо него — нет? - →Как испустить запасное значение прямо из
catchвместо проброса исключения?