В чём разница между холодными (cold) и горячими (hot) flows?
Cold Flow запускает producer-блок заново при каждом collect (flow builder, callbackFlow). Hot Flow (StateFlow, SharedFlow) существует независимо от подписчиков и разделяется между несколькими collectors. Превратить cold в hot можно через shareIn() или stateIn().
Cold Flow — ленивый, независимый для каждого collector
Холодный (cold) Flow не производит данные, пока нет активного collect. Каждый вызов collect запускает producer-блок заново, независимо от других subscribers. Стандартный flow { emit(...) } builder всегда создаёт холодный Flow.
fun fetchUser(id: String): Flow<User> = flow {
println("Fetching user $id...") // выполняется при каждом collect
val user = api.getUser(id) // HTTP-запрос
emit(user)
}
fun main() = runBlocking {
val userFlow = fetchUser("42")
userFlow.collect { println("Collector 1: $it") } // HTTP-запрос #1
userFlow.collect { println("Collector 2: $it") } // HTTP-запрос #2 — producer запущен снова!
}
Hot Flow — активен независимо от subscribers
Горячий (hot) Flow производит данные независимо от наличия collectors. Новый subscriber получает только те значения, которые эмитятся после его подписки (или с replay, если настроен). Примеры: StateFlow, SharedFlow, Channel.receiveAsFlow().
class SensorViewModel : ViewModel() {
// SharedFlow — hot, broadcast нескольким collectors
private val _sensorData = MutableSharedFlow<SensorReading>(
replay = 1, // новый subscriber получит последнее значение
extraBufferCapacity = 16
)
val sensorData: SharedFlow<SensorReading> = _sensorData.asSharedFlow()
// StateFlow — hot, всегда хранит текущее значение
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun onNewReading(reading: SensorReading) {
viewModelScope.launch {
_sensorData.emit(reading) // все активные collectors получат это
}
_uiState.value = UiState.Ready(reading) // немедленно обновляет состояние
}
}
// Два экрана подписываются на один поток данных
viewModel.sensorData.collect { screen1.update(it) }
viewModel.sensorData.collect { screen2.update(it) } // те же данные, не дублируется запрос
Преобразование cold в hot: shareIn и stateIn
Если нужно разделить дорогой cold Flow между несколькими collectors, используйте shareIn или stateIn:
class LocationRepository(
private val locationManager: LocationManager,
private val scope: CoroutineScope
) {
// Cold Flow из callbackFlow
private val rawLocation: Flow<Location> = locationManager.locationFlow()
// Hot SharedFlow — один listener на ОС, несколько observers в приложении
val sharedLocation: SharedFlow<Location> = rawLocation
.shareIn(
scope = scope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000),
replay = 1
)
// StateFlow — для текущей позиции
val currentLocation: StateFlow<Location?> = rawLocation
.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = null
)
}
Сравнительная таблица
- flow { } — cold, один collector, producer перезапускается при каждом collect
- channelFlow { } — cold, но разрешает send из разных корутин внутри блока
- SharedFlow — hot, broadcast, нет initial value, настраиваемый replay
- StateFlow — hot, broadcast, всегда имеет текущее значение, дедуплицирует consecutive equals
- Channel.receiveAsFlow() — hot, но один consumer (точечная доставка)
Подводные камни
- Множественный collect cold Flow — каждый collector запускает producer отдельно; HTTP-запрос, DB-запрос или тяжёлое вычисление выполняется N раз; решение:
shareIn - StateFlow дедупликация — если два последовательных значения равны по
equals, второйemitигнорируется; для one-shot событий (навигация, ошибки) используйте SharedFlow - SharedFlow с replay при подписке — новый collector немедленно получит последние
replayзначений; неожиданно если flow используется для событий навигации - SharingStarted.Eagerly vs WhileSubscribed —
Eagerlyстартует сразу и никогда не останавливается, тратя ресурсы;WhileSubscribed(5_000)останавливается через 5 с после последнего unsubscribe (Android: переживает смену конфигурации) - Утечка hot Flow в тестах — SharedFlow/StateFlow созданные с GlobalScope или отдельным scope не завершаются с тестом; передавайте TestScope
- collect блокирует до отмены scope —
hotFlow.collect { }не завершается само; нужно вызывать в отдельномlaunchили использовать операторы типаtake(n),first() - flowOn не влияет на hot Flow —
flowOn(Dispatchers.IO)меняет dispatcher только для cold upstream-части; StateFlow/SharedFlow работают на своём dispatcher
Common mistakes
- Объяснять «cold и hot flows» только как синтаксис и не описывать поведение runtime/compiler.
- Игнорировать важный риск: Путаница влияет на networking, caching и события: можно случайно сделать несколько запросов или потерять emission.
- Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.
What the interviewer is testing
- Формулирует суть темы «cold и hot flows» своими словами и связывает ее с кодом.
- Называет механизм: StateFlow и SharedFlow являются hot; flow builder обычно cold и повторно выполняет блок при collect.
- Видит production-последствие: Путаница влияет на networking, caching и события: можно случайно сделать несколько запросов или потерять emission.