flowOn and context preservation

Flow and StateFlow Senior Kotlin 2.0+ API 24+ Reviewed By Best Latest Apps

Spot the bug

This flow crashes with 'Flow invariant is violated'. Which line causes it?

  1. fun readLines(file: File): Flow<String> = flow {
  2. withContext(Dispatchers.IO) {
  3. file.useLines { lines ->
  4. lines.forEach { emit(it) }
  5. }
  6. }
  7. }
Hint 1 of 1

Which line changes the coroutine context?

Choose an answer, then open Show answer below to check it.

Show answer
  • Answer: line 2 withContext(Dispatchers.IO) {

A flow can't emit from a different context than the collector's. Remove withContext and add .flowOn(Dispatchers.IO) after the flow builder.

30-second answer

Use flowOn(Dispatchers.IO): it changes the dispatcher for the operators above it, while the collector keeps its own context. A flow must emit from the same coroutine context it was collected in, so calling emit inside withContext breaks that rule and throws IllegalStateException at runtime.

Practise saying it · 35 s. No recording, just a timer

Explain flowOn and why emit can't run in withContext.

Say your answer out loud, then check which of these points you covered:

  • flowOn changes upstream context only
  • The collector keeps its context
  • Emitting from withContext violates context preservation
  • channelFlow when you need other coroutines

Deep dive

flowOn adds a buffer between the two contexts, so the producer can run ahead of the collector.

Only the upstream is affected: flow.map { a }.flowOn(IO).map { b } runs a on IO and b in the collector's context.

channelFlow and callbackFlow allow sending from other coroutines because they use a channel.

Room and Retrofit flows are already main-safe; you rarely need flowOn for them.

Moving file reading off the main thread Compile-checked · Kotlin 2.4.21
fun readLines(file: File): Flow<String> = flow {
    file.bufferedReader().useLines { lines ->
        for (line in lines) emit(line)
    }
}.flowOn(Dispatchers.IO)

Common mistakes

  • Wrapping emit in withContext(Dispatchers.IO).
  • Expecting flowOn to change where collect { } runs.
  • Adding flowOn(IO) to every Room flow out of habit.

Senior take & follow-ups

Say 'context preservation' and explain flowOn affects upstream only. That's the senior signal.

Follow-up questions

Think of your answer first, then open the hint.

  1. Where does the collect block run after flowOn(IO)?

    Show hint

    In the collector's own context, for example Main.

  2. When would you use channelFlow?

    Show hint

    When you need to emit from several coroutines at once.

Suggest a fix if something here is wrong or out of date.