Введение: вопрос, который разделяет кандидатов
«Почему говорят, что 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».
val numbers = flow {
println("generating")
emit(1)
emit(2)
}
println("создали поток — ничего не печатается")
numbers.collect { println(it) } // теперь "generating", 1, 2
numbers.collect { println(it) } // снова "generating", 1, 2Два свойства, которые нужно назвать на собеседовании:
- Ленивость. Создание потока не запускает код. Это как рецепт: описали, что приготовить, — выполняется только когда начали готовить (
collect). - Независимость коллекторов. Два
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: они существуют сами по себе, хранят состояние или буфер и раздают значения тем, кто подписан. Подписчики не запускают никакой работы — они просто слушают.
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>: состояние одно, подписчики слушают его изменения. Пересоздавать состояние на каждого подписчика бессмысленно.
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, разберём вашу ситуацию и составим план подготовки.