skip to content

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%

answer

  1. Flow has one method: suspend collect(collector)
  2. collect { } adapts lambda to FlowCollector
  3. toList/first/reduce all built on collect
  4. first cancels upstream via AbortFlowException
  5. launchIn = scope.launch { collect() }

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.

solid answer

~40 s

collect is the fundamental terminal operator: it's a suspend function on Flow that takes a FlowCollector. The collect { action } overload wraps your lambda as a FlowCollector whose emit calls action(value). Calling it actually runs the producer block, invoking your lambda per emission, suspending the producer until each call returns (lockstep). All other terminals are conveniences over collect: toList collects into a MutableList; first collects until the first value then cancels upstream; reduce folds; count tallies. The parameterless collect() (the raw Flow.collect(collector)) is the low-level form; informally people use a no-op collect { } to just trigger and drain a flow for its side effects. Because collect is suspend, you must call it from a coroutine/suspend context; it returns only when the flow completes (or throws/cancels).

code

kotlin · 6 lines
kotlin
// All terminals reduce to collect:
val xs = flowOf(1, 2, 3)
val all  = xs.toList()                 // [1,2,3]
val head = xs.first()                  // 1, cancels the rest
val sum  = xs.reduce { a, b -> a + b }  // 6
xs.onEach { println(it) }.launchIn(scope) // collect in a coroutine

go deeper

for a junior

Knows collect { } runs the lambda per value and starts the flow.

for a middle

Knows several terminals and that collect is suspend / must run in a coroutine.

for a senior

Explains that Flow's single method is collect and how toList/first/launchIn are built on it, including first's cancellation.

for a principal

Reasons about terminal-operator design, cancellation/abort mechanics, and when to expose collect vs launchIn in an API.

## collect is THE terminal operator `Flow<T>` has exactly one abstract method: ```kotlin public interface Flow<out T> { public suspend fun collect(collector: FlowCollector<T>) } ``` Everything else is an extension built on it. Calling `collect` is what actually **starts** the cold producer. ## The collect { } lambda overload The ergonomic `collect { value -> ... }` is an extension that adapts your lambda into a `FlowCollector`: ```kotlin // Conceptually: suspend fun <T> Flow<T>.collect(action: suspend (T) -> Unit) = collect(object : FlowCollector<T> { override suspend fun emit(value: T) = action(value) }) ``` So each `emit(value)` upstream ultimately calls your `action(value)`. Since `emit` and `action` are `suspend`, the producer is paused until your block returns — **lockstep / backpressure**. ## Why other terminals are 'built on' collect - `toList()` / `toSet()` — collect each value into a `MutableList`/`MutableSet`. - `first()` — collect until the first value, then throw an internal `AbortFlowException` to cancel upstream and return. - `single()` — collect, ensure exactly one value. - `reduce` / `fold` — accumulate across collected values. - `count()` — increment per value. - `launchIn(scope)` — `scope.launch { collect() }`, a shorthand to start collection in a scope (often paired with `onEach`). ```kotlin val list = numbers.toList() // internally collects into a list val head = numbers.first() // collects one, cancels rest numbers.onEach { log(it) }.launchIn(scope) // collect in a coroutine ``` ## collect { } vs parameterless collect() - `collect(collector)` is the raw, single-abstract-method form you rarely call directly. - `collect { value -> ... }` is the everyday lambda overload. - A **no-arg-lambda** `collect { }` (empty body) simply triggers and drains the flow when you only care about side effects produced upstream (e.g. via `onEach`). Note: in current kotlinx.coroutines there is also `collect()` with no argument as a terminal that drains the flow without a per-value action; semantically equivalent to `collect {}`. ## Practical notes - `collect` is `suspend` → call from a coroutine. It does not return until the flow **completes**, so collecting an infinite flow blocks that coroutine until cancelled. - To collect without blocking the current coroutine, use `launchIn(scope)`.

  • Why does collecting an infinite flow with collect { } never return?
    collect suspends until the flow completes. An infinite producer never completes, so the collecting coroutine stays suspended until it is cancelled (e.g. by scope cancellation, take, or timeout).
  • How does first() avoid consuming the whole flow?
    After receiving the first value it throws an internal AbortFlowException that cancels the upstream producer, then returns that value — so the rest is never produced.

saying these in an interview costs you the question

  • Thinks collect is non-suspending / returns immediately
  • Believes toList uses some special engine unrelated to collect
  • Doesn't realize first cancels the upstream
  • Confuses launchIn with a blocking call

context