"Efficient and Effective Asynchronous Programming with Dispatchers in Kotlin Coroutines"

In the previous article, we introduced ourselves to coroutines and learned how we can leverage the power of coroutines. We also looked at ways we can set up coroutine scopes by using the launch { } and async { } builders. We also looked at the differences between the two. In this article, we dive further and talk about another interesting topic, Coroutine Dispatchers.

But first? What is a Dispatcher?

It is a determinant for what thread or threads the corresponding coroutine runs in. This dispatcher can confine the coroutine execution to a specific thread, dispatch it to a thread, or let it run unconfined. Think of the dispatcher as a foreman at a construction site. The foreman will oversee that work is being done and assigns tasks to various workers based on say the urgency or the complexity. If some work needs to be completed but the work might hinder other work from going on, then the dispatcher knows how to offload it into a different thread.

Creating a Dispatcher

To create a dispatcher. we must be inside a coroutine scope. Remember the analogy we used earlier about the construction site? A foreman (dispatcher) cannot be in control unless they are within the environs of a coroutine scope. Here, the dispatcher as well has to be inside a coroutine scope.

There are four types of dispatchers namely:

  1. Main dispatcher - executes on the main thread. Typically, this is the thread that an application uses to draw UI on the screen. Everything runs synchronously on the main thread.

  2. IO dispatcher - offloads work onto a background thread. Used for operations like network and disk I/O calls etc. Execution happens asynchronously in this dispatcher.

  3. Unconfined dispatcher - dispatcher launched in absence of any other dispatchers that have been explicitly defined. The coroutine is started in the calling thread.

  4. `Default dispatcher` - perfect for CPU-intensive tasks like complex mathematical calculations. When no other dispatcher is explicitly defined in the scope, the execution runs on this dispatcher.

By default, everything runs on the main dispatcher, to switch dispatchers we use the withContext(dispatcher){ ... } block.

Using a dispatcher

couroutineScope.launch {
        launch {
            withContext(Dispatchers.IO) {
                // non-blocking code
                // runs inside the background thread
            }
        }

        launch {
            withContext(Dispatchers.Unconfined){
                // executed on the calling thread
            }
        }

        launch {
            withContext(Dispatchers.Default){
                //complex and resource-intensive operations
            }
        }
        // other code on the main thread
    }
}

All the code inside the withContext block will be offloaded onto the thread corresponding to the specified dispatcher.

This ensures operations like drawing the UI work efficiently as all the heavy work is already offloaded onto the appropriate thread.

And that's it for dispatchers. Additional information can be obtained here explaining dispatchers in a more granular way.