skip to content

What is the flow { } builder in Kotlin, and what does it mean that the flow it creates is 'cold'?

level: juniorimportance: must knowfreq 80%

answer

  1. flow { } returns Flow<T>, lambda is suspend FlowCollector
  2. emit(value) pushes downstream; emit is suspend
  3. Cold = re-runs per collector; lazy = nothing until collect
  4. collect is the fundamental terminal operator
  5. emit/collect lockstep => built-in backpressure

basics

~10 s

flow { } makes a stream of values. Inside the block you call emit(value) to send each item. Nothing runs until someone collects it. A cold flow re-runs its code fresh for every collector.

solid answer

~40 s

flow { } is a builder from kotlinx.coroutines that returns a Flow<T>. Its lambda is a suspending FlowCollector receiver, so inside you call emit(value) to produce each element. The builder is cold and lazy: the lambda does not execute when flow { } is called: it only runs when a terminal operator like collect { } is invoked, and it re-runs from scratch for each new collector. emit is a suspend function, so you can call other suspend functions (delay, network calls) between emissions. Because collect suspends, the producer and consumer run in lockstep (backpressure for free). Calling collect twice starts two independent executions of the producer block, each with its own state.

code

kotlin · 11 lines
kotlin
val ticks = flow {
    var n = 0
    while (true) {
        emit(n++)
        delay(1000)
    }
}

suspend fun demo() {
    ticks.take(3).collect { println(it) } // 0,1,2 then completes
}

go deeper

for a junior

Knows flow { } + emit produce values and collect consumes them; can state nothing runs until collect.

for a middle

Articulates cold vs hot, per-collector re-execution, and that emit is suspend.

for a senior

Explains lockstep emit/collect, backpressure, and the intermediate-vs-terminal distinction.

for a principal

Frames cold flows in a design context (cheap to declare, share via stateIn/shareIn when needed) and reasons about resource/lifecycle implications of re-execution.

## What flow { } is `flow { }` is a top-level builder function in `kotlinx.coroutines.flow` that constructs a `Flow<T>`. A `Flow<T>` is an asynchronous stream of zero or more values that completes either normally or with an exception. The argument to `flow { }` is a lambda with receiver of type `FlowCollector<T>`, and the lambda is marked `suspend`. That receiver gives you one key function: - `emit(value: T)` — a `suspend` function that pushes one value downstream to the collector. ```kotlin val numbers: Flow<Int> = flow { for (i in 1..3) { delay(100) // suspend work is allowed emit(i) // push a value downstream } } ``` ## What 'cold' and 'lazy' mean - **Lazy:** simply calling `flow { ... }` does NOT run the block. It just captures the recipe. No `delay`, no `emit`, nothing executes yet. - **Cold:** the producer block runs **once per collector**, from the beginning. Each call to a terminal operator (`collect`, `toList`, `first`, …) starts a fresh, independent execution. ```kotlin val f = flow { println("producing"); emit(1) } // nothing printed yet f.collect { println(it) } // prints: producing, 1 f.collect { println(it) } // prints AGAIN: producing, 1 ``` Contrast this with a "hot" source like `SharedFlow`/`StateFlow`, which emits independently of collectors and is shared between them. ## emit and collect run in lockstep `emit` is `suspend`. When you `collect`, your collector lambda runs on each emitted value, and `emit` does not return until that lambda finishes. So the producer is paused while the consumer processes a value — this gives natural **backpressure**: a fast producer cannot outrun a slow consumer. There is no internal buffer unless you add `.buffer()`. ## Terminal vs intermediate - **Intermediate operators** (`map`, `filter`, `take`) are also cold and lazy — they just wrap the flow and return a new `Flow`. - **Terminal operators** (`collect`, `toList`, `first`, `single`, `reduce`) are `suspend` functions that actually start the flow. `collect { }` is the most fundamental terminal operator; every other terminal is built on it. ## Key APIs - `flow { }`, `FlowCollector`, `emit`, `collect`, intermediate operators, terminal operators.

  • If you call collect twice on the same flow value, how many times does the producer block run?
    Twice — once per collect. Cold flows re-execute the producer for every collector, each with independent state.
  • Why is emit a suspend function?
    So it can wait for the (potentially slow) collector to process the value, giving backpressure, and so you can call other suspend functions around it.

A cold flow is like a recipe card: it does nothing on its own, and each cook who follows it makes a fresh dish from scratch.

saying these in an interview costs you the question

  • Says the flow block runs as soon as flow { } is called
  • Confuses cold flow with hot SharedFlow/StateFlow
  • Thinks two collectors share one execution of the producer
  • Claims emit buffers values so producer never waits

context