Demystifying The Kotlin Coroutine Dispatchers
Android Development Best Practices and Understanding the Different Types of Kotlin’s Coroutine Dispatchers

Writing asynchronous code, that doesn’t block the user interface (UI), is an essential part of mobile app development in general. It allows for creating a seamless user experience while running multiple tasks simultaneously without affecting the performance or stability of our apps.
Such tasks include loading data from the internet, handling heavy tasks such as image processing, or evaluating user input.
While Java was the preferred programming language for Android app development, asynchronous programming was done using callback-based approaches like the now deprecated AsyncTask.
With the paradigm shift to “Kotlin first”, which means replacing Java with Kotlin as the preferred language, we suddenly had access to a vast variety of new language features.
As a natural consequence, Kotlin Coroutines quickly became the new standard for writing such asynchronous code. The main reason for app developers was its overall simplicity and better readability in a sequence way that the previous approaches couldn’t offer.
However, this language feature can be quite complex if we dive a little bit deeper. One of the mysteries of this language is supposedly the so-called CoroutineDispatcher contained in the CoroutineContext.
In this article, we will take a closer look at the component and the different variants. We will see how each of the variants differs from the other and in which case we should use which one.
1 What is the Kotlin Coroutine Dispatcher?
A Kotlin Coroutine consists of two main elements. The first one is the Job, which runs the actual block of code of our coroutine and the Kotlin Coroutine Dispatcher.
A dispatcher is a mechanism that determines which thread a particular coroutine should run on. This is important because different tasks are best suited for different threads. For example, work that interacts with the UI should run in the main thread, while work that involves blocking I/O operations, such as reading from or writing to a file or database or running a network request, should run in a separate thread.
The Kotlin Coroutine Dispatcher comes with four built-in dispatchers that are designed for specific use cases: Dispatchers.Default, Dispatchers.Unconfined, Dispatchers.IO, and Dispatchers.Main.
In the following, we will take a closer look at each of these dispatchers and discuss when they should be used in the context of Android development.
- As the name implies, the
Dispatchers.Defaultis the default dispatcher when running coroutines. It contains a shared pool of threads that get managed by the Kotlin Coroutine Dispatcher. When a coroutine is launched with this dispatcher, it gets added to a queue of pending coroutines. The coroutine will then automatically get dequeued and executed by one of the worker threads contained in this pool. This dispatcher is a good choice for most background tasks that are not tied to any specific I/O operation. - The
Dispatchers.Unconfineddispatcher is similar to theDispatchers.Defaultdispatcher, but it does not confine coroutines to any specific thread type. It performs the initial continuation of a coroutine in the current call stack and lets the coroutine proceed in the thread used by the corresponding suspending function. This means that a coroutine launched with theDispatchers.Unconfineddispatcher may run on any thread. This can be useful in particular circumstances, for example, if we want to debug a coroutine. However, because we can’t determine on which kind of thread the coroutine will get executed, it’s a good choice to avoid this dispatcher in production code, as it can make our code less predictable and may even lead to unexpected errors. - The
Dispatchers.IOdispatcher on the other hand is designed for usage with coroutines that perform blocking I/O operations. These types of operations can take a long time to complete, and if they were performed on the main thread, they would cause our app to become unresponsive in the meantime. By using theDispatchers.IOdispatcher, we can ensure that these operations are performed on a separate thread, allowing our app to remain responsive while they are running. - The last dispatcher we want to discuss is the
Dispatchers.Main. This dispatcher is designed for use with coroutines that perform operations that are related to our UI. These types of operations are required to be performed on the main thread because the Android UI toolkit is not thread-safe and can only be accessed from the actual main thread. As the name implies, by using theDispatchers.Maindispatcher, we ensure that our coroutines that interact with the UI are always executed on the main thread. Internally it uses theLooper.getMainLooper() function to run the respective code in the UI thread, where the Android framework components can be safely accessed.
To sum up, for our Android apps, we should use the Dispatchers.IO dispatcher for any coroutines that perform blocking I/O operations, and the Dispatchers.Main dispatcher for any coroutines that interact with the UI and therefore with Android UI components directly. This will ensure that our coroutines always get executed on the correct threads.
2 How to create a CoroutineScope bound to a dispatcher
In the following section, we will see how we can launch a coroutine with a specific Dispatcher and how to switch between dispatchers.
If you have no CoroutineScope available in which you could launch a new coroutine, you can simply create one of your own:
// Create a new CoroutineScope bound to the Default dispatcher
private val scope = CoroutineScope(Dispatchers.Default)Afterward, you can launch a new coroutine in that scope. As explained in the previous section of this article, because we set the dispatcher for our CoroutineScope to Dispatchers.Default, every coroutine launched in that scope will initially be executed by one of the threads, held by the shared thread pool of the Dispatchers.Default.
private fun example() {
// Launch a coroutine in the created scope
scope.launch {
// Perform a CPU-intensive task
..
}
}3 How to launch and switch to a dispatcher
Sometimes we need to run our coroutine in another Dispatcher, which differs from the configuration of our current CoroutineContext.
3.1 Launching a coroutine directly in another Dispatcher
If you don’t want to execute your coroutine on the default configured Dispatcher of your CoroutineScope but with one defined by you, you can simply launch a new coroutine by supplying the desired CoroutineContext in the following way:
private val scope = CoroutineScope(Dispatchers.Default)
private fun example() {
// Launched the coroutine directly on the Dispatches.Main
scope.launch(Dispatchers.Main) {
println
..
}
}In the example above the Dispatchers.Main will be used.
3.2 Switch to another coroutine inside a coroutine
Let’s say you first want to perform a CPU-intensive task and afterward update your UI. In this case, you may first use the Dispatchers.Default and afterward want to switch to the Dispatchers.Main dispatcher.
We can use the withContext(..) function with a CoroutineContext and the respective suspension function block supplied. The withContext(..) function will dispatch the execution of the function block to the respective Dispatcher of the supplied CoroutineContext.
After the execution is finished, the execution will shift back to the original dispatcher.
Take a look at the following sample code that gives you a hint on the functioning:
private val scope = CoroutineScope(Dispatchers.Default)
private fun example() {
// Launch a coroutine in the created scope
scope.launch {
// Perform a CPU-intensive task
..
// Will print "DefaultDispatcher-worker-x"
println(Thread.currentThread().name)
withContext(Dispatchers.Main) {
// Dispatch code to the main Android Thread
..
// Will print "main"
println(Thread.currentThread().name)
}
// Switch back to the original dispatcher
..
// Will print "DefaultDispatcher-worker-x" again
println(Thread.currentThread().name)
}
}As you can see I also added some println(..) statements to verify which Thread the current coroutine is running on.
Because we used the Dispatchers.Default for our CoroutineScope, we expect the coroutine to initially run in one of the DefaultDispatcher-worker threads.
By using the withContext(Dispatchers.Main) { .. } function, we then switch to the Main thread. After the execution of the function block is finished, we expect the coroutine to switch back to the original dispatcher, which is in our case the DefaultDispatcher-worker-x again.
Performance impact
Switching between threads can be quite costly. Consider this simple example inside the init block of a ViewModel where we only wait 100ms and print a simple text.
fun example() {
scope.launch {
val timeMeasured1 = measureTime {
Thread.sleep(100)
println("timeMeasured1")
}
println("----> Simple Case: ${timeMeasured.inWholeMilliseconds}")
val timeMeasured2 = measureTime {
Thread.sleep(100)
withContext(Dispatchers.IO) {
println("timeMeasured2")
}
}
println("----> Measuring time via measureTime2: ${timeMeasured2.inWholeMilliseconds}")
val timeMeasured3 = measureTime {
Thread.sleep(100)
viewModelScope.launch(Dispatchers.IO) {
println("timeMeasured2")
}
}
println("----> Measuring time via measureTime3: ${timeMeasured3.inWholeMilliseconds}")
}
}
}The results are quite astonishing:
> Simple case: 102ms
> Switch thread case: 589ms
> Launch a new coroutine case: 102msWhile the simple executions just like launching a new coroutine on another dispatcher took not more than 2 milliseconds, the switch to another thread with withContext(..) added an execution overhead of 487ms.
So you should really think about if you need to switch the current thread and should avoid doing it preventive even if there is no heavy lifting in the respective suspend execution block included.
4 ViewModel and Domain and Data layer best Practices
When developing Android apps and following the best practices for app architecture, you will probably have been using an MVVM (Model-View-ViewModel) architecture in one of your apps.
The android framework provides the ViewModel component for this architecture principle, which on the other hand implements the viewModelScope.
This extension property represents a CoroutineScope that is bound to the lifecycle of the ViewModel so that it automatically gets canceled as soon as the ViewModel gets cleared.
Its CoroutineContext is set to the Dispatchers.Main.Immediate. While the Dispatchers.Main uses a queue to schedule coroutines, the Dispatchers.Main.Immediate skips such a queue and performs no thread switching, at least if the current thread is already on the main thread.
Using the Dispatchers.Main.Immediate can be beneficial because we don’t need to wait for other coroutines to be finished that is running on the main thread. Instead, our update will be immediately executed and the UI get updated as soon as possible.
4.1 Working with other architecture layers
In general, every suspend function that executes CPU-intensive tasks, I/O operations, or other tasks that could require a specific dispatcher, should be responsible for defining the dispatcher by itself.
We shouldn’t expect a ViewModel for example to know if code on the domain layer in some arbitrary use case requires to be executed on the Dispatchers.IO thread pool.
The same goes for communication between the domain and data layer. Depending on the type of task, the underlying supend function should therefore switch to an appropriate dispatcher with the withContext(..) function or if no result is required, we can also launch a new coroutine in the respective use case for example.
Other known frameworks like Android Room already make use of this principle and automatically switch to an appropriate dispatcher in their respective implementation.
5 Conclusion
In summary, the Kotlin Coroutine Dispatcher is an essential part of working with Kotlin coroutines. It allows us to execute our asynchronous tasks in the right context to ensure that the concurrent tasks of our app are executed smoothly and without affecting each other.
The Kotlin Coroutine Dispatcher comes with four built-in dispatchers that are designed for specific use cases. Each of these dispatchers is well-suited for different types of tasks. For us as Android developers, it’s important to understand when and how to use them in order to create the best possible user experience.
Overall, because of its importance, it is well worth taking the time to learn about Kotlin Coroutines and develop a deep understanding of how all its components work.
In my opinion, it’s not easy to wrap your head around the principles and to know when to use which of these dispatchers. But as already stated, in most cases you should be fine if you use the Dispatchers.IO dispatcher for coroutines that execute blocking I/O operations, and the Dispatchers.Main dispatcher coroutines that interact with the UI.
However, you shouldn’t overuse thread switching and really integrate it where it’s needed because it is quite costly and increases the execution time.
I hope you had some takeaways, clap if you liked my article, make sure to subscribe to get notified via e-mail, and follow for more!




