skip to content

Cold Flows

Cold flows run their producer block again for every collector and do nothing until collected. Getting this model right — including where the producer's code actually runs — prevents most Flow surprises.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

explore

questions

21

What does flowOf(...) do, and how does it differ from a list of values?

level: juniorimportance: must knowfreq 60%

answer

  1. flowOf = shorthand for flow { emit(...) }
  2. Cold: re-emits on every collect
  3. flowOf() with no args = emptyFlow()
  4. List = eager memory; Flow = lazy stream
  5. collect is suspend

basics

~10 s

flowOf takes a fixed set of values and wraps them in a Flow, emitting each one in order when you collect it. A list just holds values; a Flow streams them on demand.

solid answer

~30 s

flowOf(a, b, c) is a convenience cold-flow builder that emits the given fixed values in order, then completes. It is essentially shorthand for flow { emit(a); emit(b); emit(c) }. Being cold, nothing runs until a terminal operator like collect() is called, and the values are re-emitted on every collection. Unlike a List (an in-memory eager collection), a Flow is a lazy stream you observe with suspending collectors, so it composes with operators (map, filter), runs inside a coroutine, and supports backpressure via suspension. There is also a zero-arg flowOf() that produces an empty flow, equivalent to emptyFlow().

code

kotlin · 7 lines
kotlin
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val nums = flowOf(10, 20, 30)
    nums.collect { println(it) } // 10 20 30
}

go deeper

for a junior

Knows flowOf emits fixed values when collected and that a flow is lazy, unlike a list.

for a middle

Explains cold semantics, equivalence to flow { emit }, and the empty/single-arg variants.

for a senior

Discusses where flowOf fits in a pipeline, backpressure via suspension, and when asFlow is preferable.

for a principal

Frames cold builders within the broader reactive/coldness model and trade-offs vs hot SharedFlow seeds.

## What flowOf is `flowOf` is one of Kotlin's **convenience cold-flow builders** in `kotlinx.coroutines.flow`. It takes a fixed, known set of values and returns a `Flow<T>` that emits them in order. ```kotlin val flow: Flow<Int> = flowOf(1, 2, 3) ``` This is equivalent to writing the more verbose `flow { }` builder: ```kotlin val flow = flow { emit(1) emit(2) emit(3) } ``` ## Cold means lazy **Cold** means the builder does nothing until a **terminal operator** (such as `collect`) runs. Each collection re-executes the emission from scratch: ```kotlin val f = flowOf(1, 2, 3) f.collect { print(it) } // prints 123 f.collect { print(it) } // prints 123 again ``` ## Flow vs List - A `List<Int>` is an **eager, in-memory collection** — all elements exist at once. - A `Flow<Int>` is a **lazy asynchronous stream** — values are produced one at a time when collected. - Collecting a flow happens inside a **coroutine**; `collect` is a `suspend` function. - Flows compose with operators like `map`, `filter`, `onEach`; suspension provides natural **backpressure**. ## Empty and single variants - `flowOf()` (no args) returns an empty flow — same as `emptyFlow<T>()`. - `flowOf(x)` (one arg) is a single-value flow. ## When to use it Use `flowOf` for **fixed, literal values** you want to feed into a flow pipeline — for tests, defaults, or seeding a chain of operators. For values you already hold in a collection, prefer `asFlow()` instead.

  • What does flowOf() with no arguments produce?
    An empty flow that completes immediately without emitting anything, equivalent to emptyFlow().
  • If you collect the same flowOf twice, what happens?
    It re-emits all values both times, because flows are cold and each collection restarts the emission.

flowOf is like a player piano roll: the notes are fixed, but they only play when someone presses start.

saying these in an interview costs you the question

  • Calling a Flow 'just a list with extra steps'
  • Thinking flowOf runs immediately when constructed
  • Believing the values are emitted only once across multiple collectors
  • Confusing flowOf with a hot stream like StateFlow

context

open as a page

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

level: juniorimportance: must knowfreq 80%

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.

open as a page

What does it mean that a Kotlin Flow is "cold", and what does that imply about when its code runs?

level: juniorimportance: must knowfreq 80%

basics

~10 s

A 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.

open as a page

In Kotlin Flow, what does 'context preservation' mean, and in which coroutine context does emit run by default?

level: juniorimportance: must knowfreq 70%

basics

~10 s

It means the code that produces values runs in the same place that collects them. By default, emit runs in the collector's coroutine, so the flow uses whatever context the collector started in.

open as a page

How does asFlow() work for Iterable and Sequence, and when would you use it over flowOf?

level: middleimportance: must knowfreq 50%

basics

~20 s

asFlow() is an extension that turns something you already have — a list, sequence, or range — into a Flow that emits each element. Use it when the values live in a collection; use flowOf for literal values you type out.

open as a page

When would you reach for the flow { } builder instead of flowOf(...) or a collection's .asFlow(), and what can flow { } do that they cannot?

level: middleimportance: must knowfreq 55%

basics

~10 s

Use flow { } when you need to compute or fetch values lazily, call suspend functions, or emit based on logic/loops. flowOf and asFlow just wrap values you already have.

open as a page

Explain what happens when the same cold Flow is collected twice. Do the two collectors share emissions or state?

level: middleimportance: must knowfreq 65%

basics

~10 s

Each collect runs the Flow's code again from scratch, completely independently. The two collectors do not share values or any internal state — it is like running the recipe twice.

open as a page

Why does emitting from a different coroutine context (e.g. inside withContext or from launch) throw, and what is the correct fix?

level: middleimportance: must knowfreq 65%

basics

~20 s

Flow requires every emit to happen in the collector's context. If you emit from inside withContext or a launched coroutine, you've changed the context, so Flow throws an error. The fix is to use flowOn or channelFlow.

open as a page

What exactly does flowOn(dispatcher) change in a Flow pipeline, and why is it described as affecting only the 'upstream'?

level: middleimportance: must knowfreq 75%

basics

~10 s

flowOn changes the context (e.g. the dispatcher) for the operators and the producer that come before it in the chain. Everything after it, including collect, keeps the collector's context.

open as a page

How do exceptions and cancellation behave in a flow { } producer, and what is the 'exception transparency' principle as it relates to emit/collect?

level: middleimportance: should knowfreq 50%

basics

~10 s

If the producer block throws, collect rethrows that exception and the flow stops. You should not wrap emit in a try/catch that swallows errors. Cancelling the collector cancels the producer too.

open as a page

How do you build a flow from a numeric range or an array, and what are the gotchas with large ranges?

level: middleimportance: should knowfreq 35%

basics

~10 s

Use (1..n).asFlow() for a range or myArray.asFlow() for an array. Each number or element is emitted in order. With huge ranges, prefer asFlow over flowOf so you don't pass thousands of varargs.

open as a page

Within a single collector, how do emit and the collector body interact? Are they concurrent or sequential?

level: middleimportance: should knowfreq 55%

basics

~10 s

They run one after another, not at the same time. emit suspends the producer until the collector finishes handling the current value, then the producer continues to the next emit.

open as a page

Explain how collect { } drives a flow, and why every other terminal operator (toList, first, reduce) is built on top of it. What is the difference between collect { } and the parameterless collect()?

level: seniorimportance: should knowfreq 35%

basics

~20 s

collect { } starts the flow and runs your lambda for each emitted value. It's the basic terminal operator. Things like toList just collect into a list internally. collect() with no lambda just drains the flow.

open as a page

What does the 'flow invariant: emission from another coroutine is not allowed' rule mean, and how is the flow { } builder enforced to be sequential and context-preserving?

level: seniorimportance: should knowfreq 45%

basics

~10 s

Inside flow { } you must call emit from the same coroutine that runs the block. You can't launch a new coroutine and emit from there. If you need that, use channelFlow instead.

open as a page

Explain the (() -> T).asFlow() and (suspend () -> T).asFlow() overloads. How do they differ from flow { emit(...) } and from flowOf(producer())?

level: seniorimportance: should knowfreq 25%

basics

~20 s

These overloads take a function (optionally suspending) and make a flow that calls it once per collection, emitting its single return value. flowOf(producer()) instead calls producer eagerly once, before collection, and reuses that fixed result.

open as a page

Compare cold Flow laziness with hot StateFlow/SharedFlow. When would you deliberately keep a flow cold, and when would you convert it to hot?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Cold flows run per collector and only on collect, so they are great for on-demand, isolated work. Hot flows always exist and share one stream, good for shared state or events many parts observe at once.

open as a page

A teammate creates a Flow that logs and makes an HTTP call, but the logs never appear. The Flow is never collected. Explain why, and how coldness should shape where you put side effects.

level: seniorimportance: should knowfreq 45%

basics

~10 s

The Flow is cold, so its code only runs on collect. Since nobody collects it, the logging and HTTP call never execute. Side effects belong inside the flow and only fire when collected.

open as a page

How does flowOn affect buffering, concurrency, and emission ordering across the dispatcher boundary?

level: seniorimportance: should knowfreq 45%

basics

~20 s

flowOn runs the producer on another dispatcher and passes items to the collector through a channel. This lets producer and collector run at the same time, but items still arrive in the order they were emitted.

open as a page

Compare flowOn, collecting inside withContext, and launchIn for controlling where flow code runs. When is each correct?

level: seniorimportance: should knowfreq 40%

basics

~10 s

flowOn sets the context for the producer side. Collecting inside withContext sets the context for the collector and everything downstream. launchIn starts collection in a scope. They solve different problems and are often combined.

open as a page

You wrap a side-effecting iterator-backed source with asFlow() and collect it twice — what happens, and how do these convenience builders preserve cold semantics?

level: seniorimportance: nice to knowfreq 18%

basics

~10 s

Because the flow is cold, collecting twice re-runs the source twice. If the source is a one-shot iterator already consumed, the second collect emits nothing. flowOf and asFlow keep this lazy, replay-per-collect behavior.

open as a page

Why did Kotlin's Flow API choose to enforce context preservation as an invariant rather than letting producers switch context freely (as RxJava does)? What does this buy and cost?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Enforcing that emit stays in the collector's context makes flows predictable and keeps cancellation and structured concurrency correct. The cost is you must use a dedicated operator (flowOn) to change threads instead of doing it inline.

open as a page