The Flow flowOn Operator
The Flow flowOn Operator
The flowOn operator changes the coroutine context for upstream operations in a Kotlin Flow without affecting downstream collectors. It is the primary mechanism for moving expensive computations, IO operations, or database queries to background threads while keeping the collector on the main thread. Understanding how flowOn separates upstream and downstream contexts is essential for building efficient, thread safe data pipelines with coroutines. By the end of this lesson, you will be able to:
- Explain how
flowOnchanges the dispatcher for upstream operators only. - Describe why downstream collectors remain unaffected by
flowOn. - Identify the internal buffering mechanism that
flowOnintroduces between contexts. - Apply
flowOncorrectly to avoid blocking the main thread in data pipelines.
How flowOn Works
The flowOn operator applies a specified CoroutineContext to all upstream operations, including emit, map, filter, and any other intermediate operators that appear before flowOn in the chain. Downstream operations such as collect, onEach, or toList continue to execute on their original context.
val dataFlow = flow {
for (i in 1..5) {
println("Emitting $i on ${Thread.currentThread().name}")
emit(i)
}
}
.map { value ->
println("Mapping $value on ${Thread.currentThread().name}")
value * 2
}
.flowOn(Dispatchers.IO)
dataFlow.collect { value ->
println("Collected $value on ${Thread.currentThread().name}")
}
In this example, the flow builder and map operator both execute on Dispatchers.IO, while collect runs on the caller's context, typically the main thread if launched from a viewModelScope or lifecycleScope.
Context Separation and Buffering
When flowOn changes the dispatcher, the runtime introduces an internal buffer between the upstream and downstream contexts. This buffer is necessary because the upstream producer and downstream consumer now run on different dispatchers and may execute concurrently. The upstream coroutine emits values into this buffer, and the downstream coroutine reads from it.
This buffering behavior means that flowOn does not simply switch threads. It creates a channel based communication path between two coroutines. The upstream coroutine runs on the specified dispatcher and sends emissions through the channel, while the downstream coroutine receives them on its own dispatcher.
// Conceptually equivalent to:
// upstream (IO) -> Channel -> downstream (Main)
This interview continues for subscribers
Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.
Become a Sponsor