Why does the UI still freeze after the repository flow adds flowOn(Dispatchers.IO)?
A repository exposes a flow of articles and moves it off the main thread with flowOn(Dispatchers.IO). The screen still drops frames — the heavy JSON parse clearly runs on the main thread, and a profiler shows it inside the collect block.
Constraint: parseHeavy must stay off the main thread while the UI update stays on it. Do not launch a second, unstructured coroutine.
// repository
fun articles(): Flow<List<Article>> = flow { emit(api.load()) }
.flowOn(Dispatchers.IO)
// view model, collected from the main dispatcher
viewModelScope.launch {
repo.articles().collect { raw ->
val parsed = parseHeavy(raw) // 300 ms of CPU work
render(parsed)
}
}
Find and fix the bug.
flowOn changes the context only for operators UPSTREAM of it — the producer and everything declared before the call. The collector always runs in the context of the coroutine that called collect, because Flow preserves context. So parseHeavy inside collect stays on the main thread; move it upstream, above flowOn.
- ✗Believing
flowOnmovescollectoff the calling dispatcher - ✗Thinking
flowOnapplies to operators placed after it in the chain - ✗Doing heavy CPU work inside the collect block on the main dispatcher
- →What does context preservation mean for a
Flow, and why is it enforced? - →What would two
flowOncalls in one chain do to the operators between them?
Why this happens
Flow guarantees context preservation: a value must be emitted in the same context the collector observes it in. The one legal way to change that context is flowOn, and it changes it for upstream only — the producer and the operators declared before the call.
Everything below it, the terminal collect included, runs in the context of the coroutine that called collect — here viewModelScope, i.e. Dispatchers.Main.
fun articles(): Flow<List<Article>> = flow { emit(api.load()) }
.map { parseHeavy(it) } // upstream → moves to IO
.flowOn(Dispatchers.IO) // the boundary: affects only what is ABOVE it
viewModelScope.launch {
repo.articles().collect { parsed -> render(parsed) } // Main: rendering only
}
If the parse must stay on the collector's side, wrap it instead:
repo.articles().collect { raw ->
val parsed = withContext(Dispatchers.Default) { parseHeavy(raw) }
render(parsed)
}