Введение: вопрос, который разделяет кандидатов

«Почему говорят, что Flow холодный, а StateFlow — горячий, и что это меняет на практике?» — вопрос, который я задаю каждому кандидату, заявившему, что «работал с Flow».

Холодность и горячесть — базовая концепция реактивных потоков, и на ней держится всё остальное: откуда берутся данные, сколько раз выполнится код источника, почему два подписчика получают разные последовательности и когда данные теряются. Без этого понимания Flow превращается в «магию, которая как-то работает».

Термины «холодный» и «горячий» пришли из мира реактивных стримов (RxJava, Reactor) и в Kotlin Coroutines закрепились официально: документация делит все Flow именно на эти два типа. Поэтому формулировки из доки — безопасная база ответа, а вот глубина понимания проверяется примерами.

В статье разберу: что такое холодный поток, что такое горячий, чем они отличаются по поведению и как определять тип потока на собеседовании. Всё — по официальной документации Kotlin.

Холодные потоки: ленивый производитель

Холодный поток не выполняет никакой работы до момента сбора. Код построителя flow {} запускается только при вызове collect, и каждый новый коллектор запускает новое, независимое исполнение. Официальная документация формулирует это так: «cold flows start producing values when collected. Each collector triggers a new, independent execution of the flow».

Example.kt
val numbers = flow {
    println("generating")
    emit(1)
    emit(2)
}

println("создали поток — ничего не печатается")
numbers.collect { println(it) } // теперь "generating", 1, 2
numbers.collect { println(it) } // снова "generating", 1, 2

Два свойства, которые нужно назвать на собеседовании:

  1. Ленивость. Создание потока не запускает код. Это как рецепт: описали, что приготовить, — выполняется только когда начали готовить (collect).
  2. Независимость коллекторов. Два collect — два полных запуска с начала. Если внутри потока сетевой запрос — он выполнится для каждого подписчика отдельно.

Холодные потоки создаются построителем flow {}, а также функциями flowOf() и .asFlow() — все три перечислены в документации как способы создания холодного потока. Проще говоря, всё, что создано «вручную» из значений или диапазона, — холодное.

Сравнение с Sequence здесь уместное и документально подтверждённое: холодные потоки ленивы «как последовательности». Но есть принципиальное отличие — внутри flow {} можно вызывать suspend-функции и эмитить значения асинхронно, чего Sequence не умеет. Формула для собеседования: «Flow — это асинхронная и ленивая последовательность».

Формула

Cold — «работает, когда попросили, и для каждого — заново». Каждый коллектор получает свою полную последовательность, код источника выполняется столько раз, сколько подписчиков.

Горячие потоки: независимый вещатель

Горячий поток производит значения независимо от коллекторов и рассылает один и тот же поток значений всем подписчикам. Документация: «hot flows emit values independently of collectors and share the same stream of values with all collectors».

Классика в Android — StateFlow и SharedFlow: они существуют сами по себе, хранят состояние или буфер и раздают значения тем, кто подписан. Подписчики не запускают никакой работы — они просто слушают.

Ticker.kt
private val _ticks = MutableSharedFlow<Unit>(replay = 0)
val ticks: SharedFlow<Unit> = _ticks

// генерация идёт независимо от подписчиков
scope.launch {
    while (true) {
        _ticks.emit(Unit)
        delay(1_000)
    }
}

Практические следствия горячести:

  • Данные не ждут подписчиков. Событие, отправленное без слушателей, теряется (при replay = 0).
  • Поздний подписчик не получит прошлое. Он видит только новые значения — либо те, что лежат в replay-кэше.
  • Два подписчика получают один и тот же поток, а не две независимые генерации.

Параметр replay — это тот самый «кэш прошлого»: MutableSharedFlow(replay = 1) отдаст новому подписчику последнее значение, а replay = 0 — только будущие. Для StateFlow replay-логика встроена в конструктор: новый коллектор всегда сразу получает текущее значение. Эти детали часто спрашивают, потому что по ним видно, работал ли кандидат с горячими потоками вживую или только читал статьи.

Типичная ошибка на собеседовании: «StateFlow — это Flow, который хранит значение». Формально так, но главное упускается: StateFlow — горячий, и это меняет всё. Холодный источник пересоздаётся на каждого коллектора, горячий — один на всех.

Почему разница важна на практике

Разница между холодным и горячим определяет выбор инструмента в реальном проекте.

Холодный Flow — для данных, которые нужно получить заново. DAO возвращают Flow<List<Article>> — это холодный поток: каждый коллектор выполняет запрос в базу и получает свежие данные. То же с сетевыми загрузками: два экрана, собирающих один и тот же поток, сделают два независимых запроса.

Горячий StateFlow — для состояния, которое должно быть одно на всех. ViewModel держит StateFlow<UiState>: состояние одно, подписчики слушают его изменения. Пересоздавать состояние на каждого подписчика бессмысленно.

OrdersViewModel.kt
class OrdersViewModel(
    private val repository: OrdersRepository
) : ViewModel() {

    private val _orders = MutableStateFlow<List<Order>>(emptyList())
    val orders: StateFlow<List<Order>> = _orders.asStateFlow()

    fun load() {
        viewModelScope.launch {
            _orders.value = repository.loadOrders() // одно состояние на всех
        }
    }
}

Случай-ловушка: stateIn с SharingStarted.WhileSubscribed() превращает холодный поток в горячий, но только пока есть подписчики. Никто не слушает — источник останавливается. Это гибрид, и на собеседовании за его объяснение ставят плюс.

Ещё один нюанс: промежуточные операторы остаются холодными, даже если апстрим горячий. StateFlow с map не начнёт обрабатывать значения, пока его не соберут. Это тоже закреплено в документации: «intermediate operators are cold... even when the upstream flow is hot».

Буфер channelFlow по умолчанию

channelFlow — гибрид холодного построителя и горячего канала: он позволяет эмитить из нескольких корутин, а по умолчанию буферизует до 64 значений. Производители приостанавливаются, когда буфер заполнен.

Про channelFlow стоит сказать отдельно, потому что это любимая ловушка интервьюеров: построитель формально холодный (работает при сборе), но внутри — настоящий буферизованный канал с конкурентными производителями. Кандидат, который честно говорит «это уже гибрид, ближе к горячему поведению», — понимает тему глубже, чем тот, кто делит мир на два лагеря.

Как определять тип потока: шпаргалка

Самый короткий тест на собеседовании: запускается ли код источника без коллектора? Запускается — горячий. Не запускается — холодный.

Второй тест — два коллектора. Холодный поток выполнит код источника дважды, и коллекторы увидят две независимые последовательности. Горячий — один раз: оба коллектора будут слушать один и тот же поток значений. Если кандидат в ответе сразу использует эти два теста, дальше можно не уточнять — тему человек понимает.

  • Холодные: flow {}, flowOf(), asFlow(), цепочки операторов до collect, DAO-Flow.
  • Горячие: StateFlow, SharedFlow, MutableStateFlow, MutableSharedFlow.
  • Гибрид: stateIn/shareIn — холодный источник превращается в горячий с политикой запуска.
  • Тест: два коллектора. Холодный — код источника выполнится дважды. Горячий — один раз, подписчики просто слушают.
  • Практика: данные «получить заново» — холодный Flow; состояние и события — горячие StateFlow/SharedFlow.

Главная формула

Cold — «работает, когда попросили». Hot — «работает всегда, подписчики слушают». Flow холодный, StateFlow и SharedFlow горячие. Объяснили эту пару на примере двух коллекторов — тема закрыта.

Что я смотрю, когда спрашиваю про cold и hot

Я Рустем Бикбулатов, senior Android-разработчик, провожу технические собеседования и менторю в Яндекс Практикуме — больше сотни учеников и разборов за плечами. Вопрос про холодные и горячие потоки я задаю почти каждому кандидату: ответ показывает, понимает ли человек Flow или просто знает синтаксис.

Сильный ответ занимает минуту: холодный — ленивый, каждый коллектор получает своё исполнение; горячий — работает независимо и раздаёт один поток; StateFlow и SharedFlow горячие; stateIn превращает холодное в горячее. Слабый — «холодный — это когда данные только подпишешься» и пауза. Разница — в системном понимании, и именно его я закрываю с учениками при подготовке к собеседованиям: Kotlin, Coroutines, архитектура — до уровня, когда ответы складываются в логичную картину, а не в список фактов.

Нужна помощь в переходе на middle?

Бесплатная диагностика: разберу ваши пробелы в Coroutines, Flow и архитектуре и составлю план роста за 30–40 минут. Без обязательств — если менторство вам не подойдёт, честно скажу.

Итоги

Холодные и горячие потоки — фундамент, на котором стоит всё понимание Flow.

  • Холодный: ленивый, код источника выполняется на каждого коллектора заново.
  • Горячий: работает независимо от подписчиков и раздаёт один поток значений всем.
  • Инструменты: StateFlow и SharedFlow — горячие; stateIn и shareIn превращают холодное в горячее; операторы холодные всегда.

Если на собеседовании вы называете себя уверенным пользователем Flow, но теряетесь на вопросе про cold/hot — не нужно зубрить дальше. Напишите в Telegram, разберём вашу ситуацию и составим план подготовки.