Sitemap

Level Up Coding

Coding tutorials and news. The developer homepage gitconnected.com && skilled.dev && levelup.dev

Tricky Android Interview Questions: Flow & StateFlow Edition

5 min readMay 12, 2025

--

Press enter or click to view image in full size
Photo by Caleb George on Unsplash

Other articles in this series

Tricky Android Interview Questions:
Kotlin Coroutines Edition
• Flow & StateFlow Edition ← you’re here
Jetpack Compose Effects Edition
ViewModel & State Handling Edition
Kotlin Object & Data Class Edition

This isn’t another “how to use Flow” article.
This is for those interview moments when the question seems simple — but turns out not to be.

Like when someone asks what happens if you call delay() inside collectLatest.

The code compiles. It even runs. But the behavior? It’s not what most developers expect — unless they’ve already run into it.

This article is a collection of tricky Flow and StateFlow questions — not trivia, not gotchas, but real examples that test how well you understand what’s happening under the surface.

If you’re preparing for interviews, or just want to check whether your mental model of Flow matches reality, you’ll find something useful 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

Only the last value is fully processed. All previous values are interrupted during the delay and never reach the End log line.

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?

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.

What to remember

  • 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.

Unless you handle the exception manually, the entire flow will crash.

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") }

Enjoyed this article?

If it helped you, or made you rethink how Flow and StateFlow work — consider leaving a clap. It helps others discover the article.

If you’re into Android interviews, Kotlin internals, or enjoy exploring subtle edge cases — feel free to follow.
More tricky questions (and answers) are coming soon.

Press enter or click to view image in full size

Anatolii Frolov
Senior Android Developer
Writing honest, real-world Kotlin & Jetpack Compose insights.
📬 Follow me on Medium

--

--

Level Up Coding
Level Up Coding
Anatolii Frolov
Anatolii Frolov

Written by Anatolii Frolov

Android & Kotlin insights — practical tips, interview prep, no fluff.

Responses (13)

Clear and easily adaptable.
Great article.

--

Great share for the interviews

--

Very helpful article. Some where I read that, it is recommended to use Flow<List<T>> instead of plain List<T> for database operations. And, these collection of tricky Flow and StateFlow questions makes it more clear on how to use Flow effectively.

--