Flow: the Basics Quiz
KOTLIN › Flow
What best describes a Kotlin Flow?
- A suspend function that returns exactly one single value
- An asynchronous stream of values that arrive over time
- A thread pool for background work
- A replacement for a plain variable
Answer: An asynchronous stream of values that arrive over time
A Flow models a stream of multiple values over time (updates, rows, results), unlike a suspend function that returns one value once.
Inside a flow { } builder, how do you hand a value to the collector?
- return value
- emit(value)
- yield(value)
- collect(value)
Answer: emit(value)
emit(value) sends a value from the producer to the collector, one at a time.
You never collect f. What happens?
- "hi" prints immediately
- Nothing runs, a flow is lazy until a terminal operator collects it
- It throws at runtime
- 1 is emitted straight to a default built-in collector supplied by the library
Answer: Nothing runs, a flow is lazy until a terminal operator collects it
Flows are cold and lazy: creating one only describes the stream. The producer runs only when a terminal operator like collect starts it.
Why must collect be called from inside a coroutine?
- It's a blocking call
- collect is a suspend function
- Flows always use Dispatchers.IO
- To avoid memory leaks
Answer: collect is a suspend function
collect is a suspend function, so it can only be called from a coroutine or another suspend function.
Two separate coroutines each collect the same cold flow. How many times does the producer block run?
- Once only, shared between both collectors
- Twice, once independently per collector
- Zero times
- It depends on timing
Answer: Twice, once independently per collector
A cold flow re-runs its producer independently for every collector; nothing is shared by default.
Which is a genuine advantage of Flow over callback/listener APIs?
- It runs measurably faster on every single device that it is used on
- You write straight-line suspending code and get cancellation for free
- It never throws exceptions
- It doesn't need a coroutine
Answer: You write straight-line suspending code and get cancellation for free
The flow builder is a suspending context, so you call suspend functions inline (no callbacks), and collection is cancellable with the coroutine.
What can you legally do inside a flow { } block?
- Only emit constant values
- Call suspend functions like delay() or a network call, then emit their results
- Start brand new threads manually and then forget about them entirely
- Return a List directly instead of emitting
Answer: Call suspend functions like delay() or a network call, then emit their results
The flow builder is a suspending context, so you can call any suspend function and emit the results, which is what makes Flow so ergonomic.
Which of these is a terminal operator that starts the flow running?
- map
- filter
- collect
- onEach
Answer: collect
collect (like toList/first) is terminal, it actually drives the flow. map/filter/onEach are lazy intermediate operators that run nothing by themselves.
A cold flow is best described as…
- Always running, and shared across every one of its collectors
- Runs on demand when collected, independently per collector
- Cached after the first collection
- Collectable only once
Answer: Runs on demand when collected, independently per collector
Cold = nothing runs until collected, and each collector triggers its own fresh run. The opposite is a hot flow (always-on, shared).
You need a value that changes over time and is observed by the UI (e.g. current search results). Which shape fits?
- A regular suspend fun returning the value once
- A Flow that emits new results as they change
- A blocking while loop on the main thread
- A global variable polled every frame
Answer: A Flow that emits new results as they change
Values that change over time and are observed are exactly what a Flow (a stream) models; a single suspend return only gives you one snapshot.
Why might (1..1000).asFlow().collect { ... } not respond to cancellation promptly?
- Because IntRange.asFlow() creates a hot flow that ignores the collector's coroutine lifecycle completely
- Because asFlow() does not use emit() internally, so there is no automatic cancellation check between elements
- Because the collect lambda runs on Dispatchers.Default which does not support cooperative cancellation at all
- Because IntRange values are primitives, and the JVM cannot interrupt primitive iteration on any dispatcher
Answer: Because asFlow() does not use emit() internally, so there is no automatic cancellation check between elements
asFlow() converts the range directly without going through emit(). Since emit() is where flow builders check cancellation, asFlow() skips that check. Adding .cancellable() before .collect restores the cancellation check between elements.
How can you check for cancellation inside a flow { } builder between expensive operations, without calling emit()?
- Call yield(), which suspends the flow and checks for cancellation, then immediately resumes it again straight afterwards
- Call currentCoroutineContext().ensureActive() which throws CancellationException if the coroutine has been cancelled
- Call flow.isCancelled which returns a Boolean you can check and break from the producer loop manually
- Call checkCancellation() which is a stdlib function that pauses the flow, checks the Job state, then continues
Answer: Call currentCoroutineContext().ensureActive() which throws CancellationException if the coroutine has been cancelled
currentCoroutineContext().ensureActive() checks whether the collecting coroutine is still active and throws CancellationException if not. It's the manual equivalent of the check that emit() does automatically.
Which builder is right for a flow of three known constants?
- flowOf(1, 2, 3)
- flow { listOf(1, 2, 3).forEach { emit(it) } }
- channelFlow { listOf(1, 2, 3).forEach { send(it) } }
- callbackFlow { trySend(1); trySend(2); trySend(3) }
Answer: flowOf(1, 2, 3)
Nothing suspends, so the most specific builder wins. flow { } exists for producers that need to suspend, and the channel-backed builders for callbacks and concurrent emission.
What happens when you call toList() on a StateFlow?
- It suspends forever, since a hot flow never completes
- It returns a single-element list with the current value
- It throws, because terminal operators reject hot flows
- It returns an empty list immediately
Answer: It suspends forever, since a hot flow never completes
toList waits for completion, and a hot flow has no end. Use take(n).toList() for a bounded read or first() for the current value.
What does first() do once it has a value?
- Cancels the upstream, which is what makes it safe on an infinite flow
- Leaves the upstream running, so that any later calls to it are cheaper
- Buffers the remaining values for a subsequent call
- Throws if the flow would have emitted more values
Answer: Cancels the upstream, which is what makes it safe on an infinite flow
Cancelling the producer is the only way a terminal operator over an infinite source can ever return, and it is why first is the idiomatic single-value read.
How many methods does the Flow interface declare?
- One: a suspending collect
- Two: collect and emit
- Three: collect, emit, and cancel
- None: it is a marker interface
Answer: One: a suspending collect
FlowCollector holds the other one, emit. Every operator is a flow whose collect wraps the upstream collector, which is why they compose so freely.
Why is a Flow cold?
- Because the producer block is the body of collect, so each call re-runs it
- Because an internal flag defers execution until a terminal operator is run
- Because the library caches and replays the definition per collector
- Because FlowCollector refuses a second subscription
Answer: Because the producer block is the body of collect, so each call re-runs it
Coldness is a consequence of the interface shape rather than an implemented feature, just as backpressure is a consequence of emit being suspending.
What distinguishes Sequence from Flow?
- A Sequence blocks the thread while waiting; a Flow suspends
- A Sequence is eager while a Flow is lazy
- A Sequence can be collected once, whereas a Flow can be collected many times
- A Sequence cannot be transformed with operators
Answer: A Sequence blocks the thread while waiting; a Flow suspends
Both are cold and lazy. The compiler enforces the distinction by refusing suspend calls inside sequence { }, which is the language telling you which one you needed.
Which signature suits a one-off upload returning a confirmation?
- suspend fun upload(f: File): Receipt
- fun upload(f: File): Flow<Receipt>
- fun upload(f: File): StateFlow<Receipt?>
- fun upload(f: File): Channel<Receipt>
Answer: suspend fun upload(f: File): Receipt
One question, one answer, no later updates. Returning a stream would hand the caller a lifecycle and an operator chain in order to deliver a single value.