What does it mean that a Kotlin Flow is "cold", and what does that imply about when its code runs?
answer
- Nothing runs until collect
- flow {} = recipe, not execution
- Terminal operator triggers it
- Each collect re-runs from the top
- Cold vs hot (StateFlow/SharedFlow)
basics
~10 sA cold Flow does nothing on its own. The code inside it runs only when you collect it. Until someone collects, no values are produced and no work happens.
solid answer
~40 sA Flow is cold because the block passed to the flow {} builder is not executed when the Flow is created — it runs lazily, only when a terminal operator such as collect is called. Creating a Flow just builds a description (a recipe) of how to produce values; it is inert. Each call to collect starts the producer block from the beginning, so the flow is re-runnable. Contrast this with a hot source like a SharedFlow or StateFlow, which emits regardless of collectors. Cold flows are also sequential per collector: emit suspends until the collector finishes processing the previous value. This laziness means side effects (network calls, logging) inside flow {} are deferred until collection, which is the key mental model for reasoning about Flow.
code
kotlin · 7 linesfun numbers(): Flow<Int> = flow {
println("producing")
for (i in 1..3) { emit(i); delay(50) }
}
val f = numbers() // prints nothing
f.collect { print(it) } // prints "producing" then 123go deeper
Knows nothing runs until collect and that flow {} is a deferred recipe.
Distinguishes intermediate vs terminal operators and explains re-execution per collect.
Contrasts cold vs hot, relates laziness to deferred side effects and resource management.
Frames coldness as a design choice for composability and backpressure, and discusses where hot conversions belong in an architecture.
## What "cold" means In Kotlin coroutines, a `Flow<T>` is a **cold** asynchronous data stream. "Cold" means the **producer code does not run until the Flow is collected**. Creating a Flow with the `flow { ... }` builder, `flowOf(...)`, or `asFlow()` only constructs an object that *describes* how to produce values — it does not start producing them. - **Terminal operator** = an operator that actually triggers execution and consumes the stream, such as `collect`, `toList`, `first`, `single`, `reduce`, `fold`. It is `suspend` and must run inside a coroutine. - **Intermediate operator** = `map`, `filter`, `onEach`, `transform`, etc. These are also lazy: they just wrap the upstream Flow and return a new cold Flow. They run only when a terminal operator pulls values through. ## Why this matters ```kotlin fun fetchUsers(): Flow<User> = flow { println("start") // does NOT print here emit(api.getUser(1)) } val f = fetchUsers() // nothing prints, no network call f.collect { println(it) } // NOW "start" prints and the API is called ``` Because the block is deferred, **side effects are deferred too**. Logging, network requests, and database reads inside `flow {}` happen at collection time, not at construction time. ## Re-runnable on each collect Every call to `collect` runs the producer block **again, from the top**, independently. Two collectors get two separate executions: ```kotlin val ticks = flow { var i = 0 while (true) { emit(i++); delay(100) } } ticks.collect { ... } // its own counter starting at 0 ticks.collect { ... } // a fresh, independent counter starting at 0 ``` ## Sequential within one collector Inside a single collection, `emit` and the collector body run **sequentially**: `emit` suspends the producer until the collector has finished handling the current value, then resumes to produce the next. There is no concurrency between producer and consumer in a plain Flow. ## Cold vs hot - **Cold** (`flow {}`, `flowOf`, `asFlow`): per-collector execution, lazy, starts on collect. - **Hot** (`StateFlow`, `SharedFlow`, channels): exist and may emit independently of collectors; collectors share one running source. The practical takeaway: a Flow is a **lazy recipe**, not a running stream. Nothing executes until `collect`.
- If you call map on a Flow but never collect it, does the map lambda run?No. map is an intermediate operator; it returns a new cold Flow. Its lambda only runs when a terminal operator collects the chain.
- Name a terminal operator other than collect.toList, first, single, reduce, fold, or count — each starts the upstream and consumes values.
A cold Flow is like a recipe card: writing it down cooks nothing — only following it (collecting) produces the meal.
saying these in an interview costs you the question
- Saying the flow {} block runs as soon as the Flow is created
- Thinking map/filter execute immediately without a terminal operator
- Confusing Flow with StateFlow (treating it as always-emitting)
- Believing a Flow caches values from a previous collect