JavaRush /Курсы /Kotlin SELF /Flow как cold‑поток: flow { emit }, collect и отличие от ...

Flow как cold‑поток: flow { emit }, collect и отличие от Channel

Kotlin SELF
54 уровень , 2 лекция
Открыта

1. Введение

Когда впервые видишь Flow, мозг честно спрашивает: “Зачем ещё одна штука, если у нас уже есть Channel? Мы же только что научились отправлять, получать, закрывать — даже буфер включать!”. Это нормальная реакция: программисту вообще свойственно думать, что новых сущностей в мире уже достаточно, особенно в понедельник утром.

Разница в том, что именно вы моделируете. Channel — это в первую очередь коммуникация: корутины “перебрасывают” друг другу значения, как мячик. А Flow — это описание процесса получения значений: “вот как значения будут появляться, и вот как их можно забирать”. Kotlin-доки формулируют это очень прямо: Flow производит значения, только когда его активно собирают, а Channel позволяет корутинам отправлять и получать значения, причём каждое значение достаётся ровно одному получателю.

Чтобы почувствовать разницу, полезна бытовая аналогия. Channel — это как почтовый ящик (или очередь в МФЦ): кто-то кладёт письма, кто-то забирает, письма копятся (если есть место), и важно “закрыть приём”, чтобы все поняли, что новых писем не будет. Flow — это скорее как сценарий экскурсии: “сначала мы покажем зал №1, потом зал №2…”, но экскурсия реально начнётся только когда придут посетители и скажут: “давайте, ведите!”.

2. Что такое Flow<T>: описание потока, а не контейнер с данными

Когда мы говорим val list = listOf(1, 2, 3), мы держим в руках данные (контейнер уже заполнен). Когда мы говорим val channel = Channel<Int>(), мы держим в руках средство коммуникации: туда могут что-то отправить, оттуда могут что-то получить. А вот val f: Flow<Int> = ... — это, скорее, договор: “вот откуда и как будут приходить числа”.

И здесь важный психологический переключатель. Flow — это не “очередь значений”, где они уже лежат и ждут вас. Это больше похоже на функцию: вы описываете, как они будут появляться, а выполнение начнётся позже.

У Flow есть два ключевых слова, которые сегодня нужно приручить:

  • flow { ... }строитель (builder), который создаёт Flow.
  • collect { ... }сборщик (терминальная операция), который запускает выполнение потока и принимает значения.

И ещё один принцип, который стоит держать в голове с самого начала: без collect “ничего не происходит”. Это не метафора и не философия, это буквально поведение Flow: пока не собрали — поток не стартовал.

3. Минимальный Flow: flow { emit() } и collect { ... }

Сейчас мы напишем самый маленький пример, который можно назвать “привет, Flow”. В нём не будет никаких операторов преобразования (это уже следующая лекция), только источник значений и сборщик.

Обратите внимание на структуру: collectsuspend, поэтому его нужно вызывать в корутинном контексте (например, внутри runBlocking, который мы уже использовали в корутинных лекциях).


import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.collect

fun main() = runBlocking {
    val numbers = flow {
        emit(1)
        emit(2)
        emit(3)
    }

    numbers.collect { v ->
        println("v=$v")              // v=1, затем v=2, затем v=3
    }
}

Здесь важно увидеть два “слоя”.

Внутри flow { ... } мы описываем, как значения будут появляться: “выдай 1, потом 2, потом 3”.

А внутри collect { ... } мы описываем, что делать с каждым значением, когда оно появится: “напечатай”.

Никакого close() вы не видите — потому что у Flow завершение выглядит по-другому: поток заканчивается, когда код в flow { ... } заканчивается. То есть “конец потока” — это просто “мы дошли до конца блока”.

4. Cold‑поведение Flow на практике

На предыдущих примерах может показаться, что Flow — это просто “ещё один способ перебрать 1, 2, 3”. Но настоящая сила начинается, когда значения появляются не мгновенно, а в течение времени. И здесь Flow естественно дружит с корутинами: вы можете делать delay между emit.

delay внутри flow: значения появляются во времени

Это хорошее место, чтобы почувствовать: Flow — это последовательность значений, растянутая во времени, но при этом собирается она последовательно (в рамках одного collect).

import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.collect

fun main() = runBlocking {
    val ticks = flow {
        emit("tick")
        delay(200)
        emit("tock")
        delay(200)
        emit("done")
    }

    ticks.collect { word ->
        println(word)                // tick, потом tock, потом done
    }
}

Вы можете думать об этом как о “маленьком генераторе”: он выдаёт значение, затем ждёт, затем выдаёт ещё одно. И всё это происходит только потому, что кто-то вызвал collect.

Почему Flow называют cold‑потоком

Слово cold (холодный) звучит так, будто поток обиделся на вас и отказывается греть батареи. На практике смысл проще: пока нет потребителя — нет производства. Если никто не собирает значения, flow { ... } даже не начнёт выполняться.

Покажем это максимально честно: добавим println внутрь flow { ... }, чтобы увидеть момент запуска.

import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.collect

fun main() = runBlocking {
    val f = flow {
        println("Flow started")      // увидим только во время collect
        emit(10)
    }

    println("Before collect")        // Before collect
    f.collect { println("v=$it") }   // Flow started; v=10
}

Если вы запустите, вы увидите, что строка "Flow started" появится после "Before collect", то есть реально во время collect, а не в момент создания f.

И вот это — центральная идея. В Channel вы можете начать отправлять значения “в никуда” (и потом кто-то их заберёт), если есть буфер или если отправитель будет ждать. А Flow — это не очередь и не “место хранения”. Он сам по себе ничего не “копит”: он исполняется.

Два collect — два запуска

Теперь сделаем маленький эксперимент: соберём один и тот же Flow два раза. Если вы привыкли к коллекциям, может ожидаться, что “ну собрали и собрали, второй раз просто снова пройдём по тем же данным”. Но у cold‑потока другая логика: каждый collect запускает выполнение заново.

import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.collect

fun main() = runBlocking {
    val f = flow {
        println("Producing...")
        emit(1)
    }

    f.collect { println("first collect: $it") }
    // Producing...
    // first collect: 1

    f.collect { println("second collect: $it") }
    // Producing...
    // second collect: 1
}

Здесь “Producing…” выводится дважды — это и есть практический смысл cold‑потока.

Полезная мысль для закрепления: Flow ближе к “рецепту приготовления” (его можно приготовить много раз), а Channel ближе к “кастрюле супа” (туда налили — и дальше кто успел, тот и съел).

5. Flow и Channel: выбираем правильную модель

Сейчас будет важный кусок: мы не просто перечислим различия, а свяжем их с тем, какую задачу вы решаете. И здесь удобно держать в голове “модель мира”.

Channel моделирует очередь сообщений между корутинами: “кто-то отправил, кто-то получил”. В Kotlin-доках это сформулировано так: канал позволяет корутинам отправлять и получать значения, и каждое значение доставляется ровно одной корутине. Это прямо намекает на распределение работы между несколькими потребителями.

Flow моделирует поток значений во времени: “источник выдаёт значения, а потребитель их собирает”. И ключевой момент: Flow выдаёт значения только когда его собирают. Поэтому он естественно выглядит как “источник → сбор”.

Чтобы было проще сравнивать без внутренней боли, сведём основные отличия в таблицу:

Тема Channel Flow
Главная идея Коммуникация: передать сообщение между корутинами Поток значений: описать, как значения появляются во времени
Старт работы Отправитель может начать send сразу (но может ждать) Источник запускается только при collect (cold)
“Конец данных” Обычно нужен close() как сигнал завершения Завершение = окончание блока flow { ... }
Несколько потребителей Одно значение получит только один получатель Каждый collect запускает поток заново (в общем случае)
На что похоже Очередь/почта/лента задач Генератор/рецепт/сценарий выдачи

И вот типичный практический критерий выбора (без фанатизма). Если вы проектируете систему “есть воркеры, надо распределять задачи между ними”, канал ощущается естественнее. Если вы проектируете систему “есть источник данных, я хочу последовательно получить значения и обработать их”, Flow обычно читается проще и “честнее” описывает намерение.

Почему нельзя делать send в Flow (и почему это нормально)

Иногда новичок пытается использовать Flow как канал: “а как мне теперь сделать send?”. Ответ простой: никак. И это не потому, что разработчики Kotlin “забыли добавить метод”, а потому что это другой инструмент с другой задачей.

Flow — это поток из источника в потребителя. У него нет общего “ящика”, куда можно из разных мест накидать сообщений. Это сознательное ограничение: оно заставляет вас держать архитектуру чище. Источник описан в одном месте, сбор — в другом, и вы не получаете “случайных” отправок из разных углов программы, которые потом невозможно отследить.

Если вам нужна коммуникация “много корутин отправляют, много корутин получают” — это территория каналов. Если вам нужна модель “значения приходят во времени, я их собираю” — Flow звучит как родной.

6. Мини‑монитор событий: Flow в консольном приложении

Чтобы примеры не жили отдельной жизнью “в вакууме”, давайте продолжим развивать маленькое приложение, которое мы уже мысленно использовали в каналах: консольный монитор событий. Раньше мы могли бы передавать события через Channel<String>, а теперь сделаем версию, где источник событий — это Flow<String>.

Мы не делаем сеть, файлы или что-то “внешнее” (сегодня это лишнее). Мы просто имитируем события: “приложение стартовало”, “подключение установлено”, “работаем”, “завершение”.

Функция, которая возвращает Flow

Начнём с маленькой функции, которая создаёт поток. Здесь важно привыкнуть: в отличие от Channel, где вы создаёте объект и потом в него “пушите”, у Flow вы чаще пишете функцию “как получать значения”.

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

fun buildStatusFlow(): Flow<String> = flow {
    emit("START")
    delay(150)
    emit("CONNECTED")
    delay(150)
    emit("RUNNING")
    delay(150)
    emit("STOP")
}

Здесь мы аккуратно написали “рецепт” статусов. Он ничего не печатает и никого не запускает — он просто возвращает Flow<String>.

collect в main: запускаем и печатаем

Теперь используем это в main. Да, опять runBlocking: мы в консольном приложении, а collectsuspend, значит нам нужен корутинный контекст.

import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.flow.collect

fun main() = runBlocking {
    val statusFlow = buildStatusFlow()

    statusFlow.collect { status ->
        println("status=$status")
        // status=START, затем CONNECTED, RUNNING, STOP
    }
}

Обратите внимание на важную дисциплину: поток описывает значения, а main решает, что с ними делать. Это делает код предсказуемее: источник не печатает “тайком” (если вы сами этого не захотите), и вы не теряете контроль.

Демонстрация cold‑поведения: собрать один поток дважды

И чтобы окончательно вбить в память cold‑природу, можно сделать маленький “режим повтора”: собрать один и тот же поток дважды.

import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.flow.collect

fun main() = runBlocking {
    val statusFlow = buildStatusFlow()

    println("First run:")
    statusFlow.collect { println(it) }     // START ... STOP

    println("Second run:")
    statusFlow.collect { println(it) }     // START ... STOP (заново)
}

Если вы увидели, что статусы повторились — поздравляю, вы только что почувствовали cold‑поток, а не просто прочитали определение.

7. Типичные ошибки при первом знакомстве с Flow

Ошибка №1: ждать, что Flow начнёт работать сам по себе.
Очень частая ловушка — написать val f = flow { emit(1) } и ожидать, что “ну он же создан, значит где-то там 1 уже пошла”. Нет: cold‑поток не начнёт выполняться, пока вы явно не вызовете collect. Если вы видите “ничего не выводится”, первым делом проверьте, есть ли сборщик.

Ошибка №2: путать Flow с очередью сообщений и искать send.
Если вы начинаете “переводить” свой код с Channel на Flow механически, вы быстро упрётесь в вопрос “а куда отправлять?”. Ответ: Flow не про “отправлять”, а про “выдавать”. Обычно это означает, что вместо “создали канал и раздаём его всем” вы пишете функцию, которая возвращает Flow, и описываете производство значений внутри flow { ... }.

Ошибка №3: забывать, что collectsuspend, и пытаться вызвать его из обычной функции.
Это выглядит как типичная компиляторная ошибка “Suspend function 'collect' should be called only from a coroutine or another suspend function”. Лечится ровно тем, что вы уже умеете: либо вызываете collect внутри runBlocking (в консольном main), либо делаете вашу функцию suspend и вызываете её из корутины.

Ошибка №4: удивляться, что два collect делают работу дважды.
Новички часто воспринимают Flow как “список, только асинхронный”. Поэтому повторный collect кажется “повторным чтением тех же данных”. На деле это повторный запуск. Это полезно (можно переиспользовать поток как рецепт), но если вы не ожидали — вы можете случайно выполнить дорогую работу два раза. На старте курса достаточно просто помнить правило: cold‑поток обычно перезапускается на каждый сбор.

Ошибка №5: смешивать ответственность источника и потребителя.
Когда внутри flow { ... } начинают печатать в консоль, менять глобальные переменные и делать “пол‑приложения”, код быстро становится мутным: непонятно, кто управляет выводом и когда что происходит. На первых шагах лучше держать привычную дисциплину: источник делает emit, потребитель делает collect и решает, что выводить и как реагировать. Это делает поведение понятнее, особенно когда вы позже начнёте собирать более длинные пайплайны.

1
Задача
Kotlin SELF, 54 уровень, 2 лекция
Недоступна
Три сигнала
Три сигнала
1
Задача
Kotlin SELF, 54 уровень, 2 лекция
Недоступна
Холодный запуск
Холодный запуск
1
Задача
Kotlin SELF, 54 уровень, 2 лекция
Недоступна
Двойной сбор
Двойной сбор
1
Задача
Kotlin SELF, 54 уровень, 2 лекция
Недоступна
Статусы системы
Статусы системы
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ