5 minutes
Tricky Android Interview Questions: Flow & StateFlow Edition
This isn’t another “how to use coroutines” article.
This is for those interview moments when the question sounds simple — but answering it isn’t. Like when you’re asked what can go into a CoroutineContext, and then immediately hit with:
What happens if you pass Job() + Job() + Job() to a CoroutineScope?
The code is valid. But the behavior? Not what you’d expect — unless you know the internals.
This article is a collection of tricky coroutine questions — not academic puzzles, not trivia, but real examples that reveal how deep your understanding actually goes.
Whether you’re preparing for interviews or just want to challenge what you think you know, you’ll find value here.
Question 1: What happens if you call delay() inside collectLatest?
Most developers understand that collectLatest cancels the previous block when a new value is emitted. But what actually happens if that block contains a suspending function like delay()?
Here’s an example that illustrates this clearly:
fun main() = runBlocking {
val flow = flowOf(1, 2, 3)
flow
.onEach { delay(90) } // simulating heavy upstream work (e.g. API, DB)
.collectLatest { value ->
println("Start $value")
delay(100) // simulating processing of each item
println("End $value")
}
}
In this setup:
- delay(90) simulates upstream latency — for example, a delay between incoming events, API responses, or database reads.
- delay(100) inside collectLatest represents the time it takes to process each item.
Because each new item arrives slightly before the previous one has finished processing, collectLatest cancels the current block before it completes.
Why this is a trick question
It’s easy to assume that collectLatest just skips items that arrive too fast. But it does more than that — it actively cancels the previous block of code, including any suspend calls like delay().
If you’re not aware of this behavior, it’s natural to assume the collector runs sequentially and completes each block.
What actually happens
Here’s the output:
Start 1
Start 2
Start 3
End 3
What to remember
- collectLatest cancels the entire lambda when a new value is emitted.
- Any suspending operation inside — including delay(), withContext, or long-running work like network or database access — will be cancelled.
- Use collectLatest only when intermediate results can be safely discarded.
Bonus insight
It’s not just delay() that gets cancelled. If you perform network or database operations inside collectLatest, those too will be interrupted if a new value arrives.
That means:
- Requests may be aborted before completion
- Database writes might never finish
- Side effects can silently disappear
If the result of an operation matters and must be completed, collectLatest is the wrong choice. In those cases, use collect {} and manage cancellation manually if needed.
Question 2: What happens if you call collect() twice on the same Flow?
It sounds simple: an exception is thrown, so the program should crash — right? Not quite.
Why this is a trick question
Most developers know that Flow is cold by default. But not everyone fully realizes what that means in practice — especially when the same flow is collected more than once.
Consider this example:
val flow = flow {
println("Flow started")
emit(1)
}
flow.collect { println("First: $it") }
flow.collect { println("Second: $it") }
The code is valid. It compiles. It runs without errors. But the behavior? Not what you might expect — unless you understand how cold flows work.
Why this is a trick question
It’s easy to assume that calling collect() twice on the same flow simply reuses the data. But that’s not how Flow behaves.
Each call to collect() starts the flow from scratch. That means the block inside flow {} is executed again — including any side effects or long-running operations.
In this example, println(“Flow started”) runs twice, even though we only defined the flow once.
What actually happens
Flow started
First: 1
Flow started
Second: 1
Each call to collect() triggers a new execution of the flow builder block.
If the flow includes something expensive — like a network request or database read — that operation will be repeated as well.
Why this question exists
- Flow is cold — each time you call collect(), it restarts from the beginning.
- Side effects inside flow {} are not shared between collectors.
- To share a single execution across collectors, use shareIn() or stateIn().
Question 3: What happens if a Flow throws an exception?
Many developers assume that collect {} simply processes values — and that even if something goes wrong, the flow will continue with the next value.
But what actually happens when a flow throws an exception during emission?
Let’s look at a minimal example:
val flow = flow {
emit(1)
throw RuntimeException("Something went wrong")
}
flow.collect { value ->
println("Received: $value")
}
The code looks simple: emit one value, then throw an exception. But the way Flow handles errors might not match your expectations.
Why this is a trick question
It’s easy to assume that collect {} will handle values as they come, and that errors would somehow be ignored unless explicitly caught.
In reality, if a Flow throws an uncaught exception, the collection is immediately cancelled. No further emissions are processed, and no further code inside collect will execute.
What actually happens
Here’s the output:
Received: 1
Exception in thread "main" java.lang.RuntimeException: Something went wrong
The first value is collected and printed. But when the exception is thrown, the flow terminates immediately. If there were more emissions after the exception, they would never be processed.
What to remember
- Unhandled exceptions inside a flow cancel the collection immediately.
- No further emissions are processed after an exception.
- To handle errors gracefully, use the catch {} operator before collect().
Example:
flow
.catch { e -> println("Caught error: ${e.message}") }
.collect { value -> println("Received: $value") }