skip to content

What do the map and filter intermediate operators do on a Kotlin Flow, and why are they 'cold' and 'lazy'?

level: juniorimportance: must knowfreq 80%

answer

  1. map = 1-in/1-out transform
  2. filter = keep where predicate true
  3. cold = re-runs per collector
  4. lazy = nothing until collect
  5. filter before map saves work

basics

~10 s

map turns each emitted value into a new value; filter keeps only values matching a condition. They do nothing until a terminal operator (like collect) runs the flow, and each collection re-runs the work.

solid answer

~40 s

map { } applies a transform to every upstream value and emits one result per input. filter { } emits only values whose predicate returns true. Both are intermediate operators: they return a new Flow and run nothing on their own. A Flow is cold — the producer block executes per collector — and the operator chain is lazy, executing only when a terminal operator such as collect, toList, or first is invoked. The lambdas in map and filter are suspend-capable (they run in a coroutine), so you can call suspend functions inside them. Operator order matters for performance: filtering before mapping avoids transforming values you will discard.

code

kotlin · 5 lines
kotlin
val result = flowOf(1, 2, 3, 4, 5)
    .filter { it % 2 == 1 }   // 1, 3, 5
    .map { it * it }          // 1, 9, 25
    .toList()                  // terminal -> runs the chain
println(result) // [1, 9, 25]

go deeper

for a junior

Knows map transforms 1:1 and filter drops by predicate, and that collect triggers execution.

for a middle

Explains cold vs lazy precisely and why two collectors re-run the producer.

for a senior

Discusses operator ordering for cost, suspend lambdas, and order preservation.

for a principal

Frames cold/lazy semantics as a design contract enabling backpressure, cancellation, and per-collector context.

## What an intermediate operator is A `Flow<T>` is an asynchronous cold stream of values. An **intermediate operator** takes a flow and returns a *new* flow, describing a transformation but executing nothing yet. The chain only runs when a **terminal operator** (e.g. `collect`, `toList`, `first`, `single`) is applied. ## map `map` transforms each value, emitting exactly one output per input: ```kotlin flowOf(1, 2, 3) .map { it * 10 } // emits 10, 20, 30 .collect { println(it) } ``` The lambda has signature `suspend (T) -> R`, so it can call other `suspend` functions. ## filter `filter` keeps only values for which the predicate returns `true`: ```kotlin flowOf(1, 2, 3, 4).filter { it % 2 == 0 } // emits 2, 4 ``` Related: `filterNot` (inverse), `filterNotNull`, `filterIsInstance<T>()`. ## Cold and lazy - **Cold**: the producer code inside `flow { }` (or `flowOf`) runs *each time* a collector collects. Two collectors get two independent executions. (Contrast: a hot `StateFlow`/`SharedFlow` exists independently of collectors.) - **Lazy**: building the operator chain allocates flow objects but runs no transform logic until a terminal operator pulls values. ## Order matters ```kotlin list.asFlow() .filter { it.isActive } // discard early .map { expensiveTransform(it) } // only transform survivors ``` Filtering before mapping avoids wasted work. Both operators preserve emission order and run sequentially in the collector's coroutine by default (no concurrency unless you add `flowOn`/`buffer`).

  • Can you call a suspend function inside map?
    Yes. map's lambda is suspend (T) -> R, so suspending calls like a network fetch are allowed; they suspend the collector's coroutine.
  • How is map on Flow different from map on a List?
    List.map is eager and synchronous, producing a new list immediately. Flow.map is lazy, asynchronous, suspend-capable, and runs only on collection.

Like a recipe card: writing the steps (map/filter) cooks nothing; only when someone actually cooks (collect) do the ingredients get processed.

saying these in an interview costs you the question

  • Saying map/filter execute immediately when written
  • Claiming a cold Flow runs once and shares results across collectors
  • Thinking map can emit zero or many values (that's transform)
  • Believing operator order never affects performance

context