skip to content

map / filter / transform

map, filter, onEach, take, and drop cover the common cases, and transform is the general form that can emit any number of values per input. Knowing transform exists is what lets you avoid contorted map-then-flatten chains.

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

questions

5

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

open as a page

What is the transform { } operator and how does it differ from map and filter? When must you reach for it?

level: middleimportance: must knowfreq 60%

basics

~10 s

transform is the general operator: for each incoming value you can emit zero, one, or many results by calling emit() as often as you like. map and filter are restricted special cases of it.

open as a page

Explain take and drop on a Flow. What is special about how take(n) terminates the upstream, and what exception underlies it?

level: middleimportance: should knowfreq 45%

basics

~20 s

take(n) lets through only the first n values then stops the flow; drop(n) skips the first n and emits the rest. take cancels the upstream once it has enough by throwing an internal exception that take catches.

open as a page

What is onEach and how does it differ from collect? Why is onEach often paired with launchIn, and what does map do that onEach intentionally does not?

level: seniorimportance: should knowfreq 40%

basics

~20 s

onEach runs a side effect for each value but passes the value through unchanged, returning a Flow. collect is terminal and returns Unit. onEach + launchIn lets you start the flow in a scope without writing a collect lambda.

open as a page

How do intermediate operators like map/filter/transform behave with respect to coroutine context and operator fusion? Why does calling withContext inside a map lambda violate context preservation, and what should you use instead?

level: principalimportance: nice to knowfreq 25%

basics

~10 s

By default every operator and emission runs in the collector's coroutine context (context preservation). You must not switch dispatcher inside map with withContext; instead change the upstream context declaratively with flowOn.

open as a page