Kotlin CoroutinesMiddleTechnical

В чём разница между холодными (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 WhileSubscribedEagerly стартует сразу и никогда не останавливается, тратя ресурсы; WhileSubscribed(5_000) останавливается через 5 с после последнего unsubscribe (Android: переживает смену конфигурации)
  • Утечка hot Flow в тестах — SharedFlow/StateFlow созданные с GlobalScope или отдельным scope не завершаются с тестом; передавайте TestScope
  • collect блокирует до отмены scopehotFlow.collect { } не завершается само; нужно вызывать в отдельном launch или использовать операторы типа take(n), first()
  • flowOn не влияет на hot FlowflowOn(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.

Sources

Related topics