Mastering Kotlin Coroutines In Android - Step By Step Guide

I am writing this article to share my knowledge on the Kotlin Coroutines which I have learned the hard way.

This article is for anyone who is curious about the Kotlin Coroutines but has no idea what it is exactly. The goal is to make you understand what are Kotlin Coroutines which means that there are few simplifications done while writing this. If you understand what Kotlin Coroutines are, then my mission will be accomplished. If you read this article completely, I am sure my mission will be accomplished.

Knowledge comes to those who crave for it.

In this blog, we are going to master the Kotlin Coroutines in Android by covering the following topics:

  • What exactly Coroutines are?
  • Why there is a need for the solution which Kotlin Coroutines provide?
  • Step by step guide on how to implement the Kotlin Coroutines in Android.
  • What are scopes in Kotlin Coroutines?
  • Exception handling in Kotlin Coroutines.

The current framework which is available to handle multithreading leads to callback hells and blocking states because we do not have any other simple way to guarantee thread safe execution.

Coroutines, a very efficient and complete framework to manage concurrency in a more performant and simple way.

Let's understand what exactly Coroutines are in a very simple way.

What are Coroutines?

Coroutines = Co + Routines

Here, Co means cooperation and Routines means functions.
It means that when functions cooperate with each other, we call it as Coroutines.

Let's understand this with an example. I have written the below code in a different way just for the sake of understanding. Suppose we have two functions as functionA and functionB.

functionA as below:

fun functionA(case: Int) {
    when (case) {
        1 -> {
            taskA1()
            functionB(1)
        }
        2 -> {
            taskA2()
            functionB(2)
        }
        3 -> {
            taskA3()
            functionB(3)
        }
        4 -> {
            taskA4()
            functionB(4)
        }
    }
}

And functionB as below:

fun functionB(case: Int) {
    when (case) {
        1 -> {
            taskB1()
            functionA(2)
        }
        2 -> {
            taskB2()
            functionA(3)
        }
        3 -> {
            taskB3()
            functionA(4)
        }
        4 -> {
            taskB4()
        }
    }
}

Then, we can call the functionA as below:

functionA(1)

Here, functionA will do the taskA1 and give the control to the functionB to execute the taskB1.

Then, functionB will do the taskB1 and give the control back to the functionA to execute the taskA2 and so on.

The important thing is that functionA and functionB are cooperating with each other.

With Kotlin Coroutines, the above cooperation can be done very easily which is without the use of when or switch case which I have used in the above example for the sake of understanding.

Now that, we have understood what are coroutines when it comes to cooperation between the functions. There are endless possibilities which open up because of the cooperative nature of functions.

A few of the possibilities are as follows:

  • It can execute a few lines of functionA and then execute a few lines of functionB and then again a few lines of functionA and so on. This will be helpful when a thread is sitting idle not doing anything, in that case, it can execute a few lines of another function. This way, it can take the full advantage of thread. Ultimately the cooperation helps in the multitasking.
  • It will enable writing asynchronous code in a synchronous way. We will talk about this later in this article.

Overall, the Coroutines make the multitasking very easy.

So, we can say that Coroutines and the threads both are multitasking. But the difference is that threads are managed by the OS and coroutines by the users as it can execute a few lines of function by taking advantage of the cooperation.

It's an optimized framework written over the actual threading by taking advantage of the cooperative nature of functions to make it light and yet powerful. So, we can say that Coroutines are lightweight threads. A lightweight thread means it doesn’t map on the native thread, so it doesn’t require context switching on the processor, so they are faster.

What does it mean when I say “it doesn’t map on the native thread”?

Coroutines are available in many languages. Basically, there are two types of Coroutines:

  • Stackless
  • Stackful

Kotlin implements stackless coroutines — it’s mean that the coroutines don’t have own stack, so they don’t map on the native thread.

Now, you can understand the below paragraph, what the official website of Kotlin says

One can think of a coroutine as a light-weight thread. Like threads, coroutines can run in parallel, wait for each other and communicate. The biggest difference is that coroutines are very cheap, almost free: we can create thousands of them, and pay very little in terms of performance. True threads, on the other hand, are expensive to start and keep around. A thousand threads can be a serious challenge for a modern machine.

Coroutines do not replace threads, it’s more like a framework to manage it.

The exact definition of Coroutines: A framework to manage concurrency in a more performant and simple way with its lightweight thread which is written on top of the actual threading framework to get the most out of it by taking the advantage of cooperative nature of functions.

Now that, we have understood what exactly Coroutines are. Now we need to know why there is a need for the solutions which Kotlin Coroutines provide.

Apply Now: MindOrks Android Online Course and Learn Advanced Android

Why there is a need for Kotlin Coroutines?

Let's take very standard use-case of an Android Application which is as follows:

  • Fetch User from the server.
  • Show the User in the UI.
fun fetchUser(): User {
    // make network call
    // return user
}

fun showUser(user: User) {
    // show user
}

fun fetchAndShowUser() {
    val user = fetchUser()
    showUser(user)
}

When we call the fetchAndShowUser function, it will throw the NetworkOnMainThreadException as the network call is not allowed on the main thread.

There are many ways to solve that. A few of them are as follows:

1. Using Callback: Here, we run the fetchUser in the background thread and we pass the result with the callback.

fun fetchAndShowUser() {
    fetchUser { user ->
        showUser(user)
    }
}

2. Using RxJava: Reactive world approach. This way we can get rid of the nested callback.

fetchUser()
        .subscribeOn(Schedulers.io())
        .observerOn(AndroidSchedulers.mainThread())
        .subscribe { user ->
            showUser(user)
        }

3. Using Coroutines: Yes, coroutines.

suspend fun fetchAndShowUser() {
     val user = fetchUser() // fetch on IO thread
     showUser(user) // back on UI thread
}

Here, the above code looks synchronous, but it is asynchronous. We will see how is it possible.

Implementation of Kotlin Coroutines in Android

Add the Kotlin Coroutines dependencies in the Android project as below:

dependencies {
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

Now, our function fecthUser will look like below:

suspend fun fetchUser(): User {
    return GlobalScope.async(Dispatchers.IO) {
        // make network call
        // return user
    }.await()
}

Don't worry, we will learn suspend, GlobalScope, async, await, and Dispatchers.IO one by one gradually in this article.

And the fetchAndShowUser like below:

suspend fun fetchAndShowUser() {
    val user = fetchUser() // fetch on IO thread
    showUser(user) // back on UI thread
}

And the showUser function as below which is same as it was earlier:

fun showUser(user: User) {
    // show user
}

We have introduced two things here as follows:

  • Dispatchers: Dispatchers helps coroutines in deciding the thread on which the work has to be done. There are majorly three types of Dispatchers which are as IO, Default, and Main. IO dispatcher is used to do the network and disk related work. Default is used to do the CPU intensive work. Main is the UI thread of Android. In order to use these, we need to wrap the work under the async function. Async function looks like below.
suspend fun async() // implementation removed for brevity
  • suspend: Suspend function is a function that could be started, paused and resume.

Suspend functions are only allowed to be called from a coroutine or another suspend function. You can see that the async function which includes the keyword suspend. So, in order to use that, we need to make our function suspend too.

So, the fetchAndShowUser can only be called from the another suspend function or a coroutine. We can't make the onCreate function of an activity suspend, so we need to call it from the coroutines like below:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    GlobalScope.launch(Dispatchers.Main) {
        fetchAndShowUser()
    }
    
}

Which is actually

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    GlobalScope.launch(Dispatchers.Main) {
        val user = fetchUser() // fetch on IO thread
        showUser(user) // back on UI thread
    }
    
}

showUser will run on UI thread because we have used the Dispatchers.Main to launch it.

There are two functions in Kotlin to start the coroutines which are as follows:

  • launch{}
  • async{}

Launch vs Async in Kotlin Coroutines

The difference is that the launch{} does not return anything and the async{}returns an instance of Deferred<T>, which has an await()function that returns the result of the coroutine like we have future in Java in which we do future.get() to the get the result.

Let's take an example to learn launch and async.

We have a function fetchUserAndSaveInDatabase like below:

fun fetchUserAndSaveInDatabase() {
    // fetch user from network
    // save user in database
    // and do not return anything
}

Now, we can use the launch like below:

GlobalScope.launch(Dispatchers.IO) {
    fetchUserAndSaveInDatabase() // do on IO thread
}

As the fetchUserAndSaveInDatabase do not return anything, we can use the launch.

But when we need the result back, we need to use the async.

We have two functions which return User like below:

fun fetchFirstUser(): User {
    // make network call
    // return user
}

fun fetchSeconeUser(): User {
    // make network call
    // return user
}

No need to make the above functions as suspend as we are not calling any other suspend function from them.

Now, we can use the async like below:

GlobalScope.launch(Dispatchers.Main) {
    val userOne = async(Dispatchers.IO) { fetchFirstUser() }
    val userTwo = async(Dispatchers.IO) { fetchSeconeUser() }
    showUsers(userOne.await(), userTwo.await()) // back on UI thread
}

Here, it makes both the network call in parallel, await for the results and then call the showUsers function.

So, now that, we have understood the difference between the launch function and the async function.

There is something called withContext.

suspend fun fetchUser(): User {
    return GlobalScope.async(Dispatchers.IO) {
        // make network call
        // return user
    }.await()
}

withContext is nothing but an another way writing the async where we do not have to write await().

suspend fun fetchUser(): User {
    return withContext(Dispatchers.IO) {
        // make network call
        // return user
    }
}

Scopes in Kotlin Coroutines

Scopes in Kotlin Coroutines are very useful because we need to cancel the background task as soon as the activity is destroyed. Here, we will learn how to use scopes to handle these types of situation.

Assuming that our activity is the scope, the background task should get canceled as soon as the activity is destroyed.

In the activity, we need to implement CoroutineScope.

class MainActivity : AppCompatActivity(), CoroutineScope {

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    private lateinit var job: Job

}

In the onCreate and onDestory function.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    job = Job() // create the Job
}

override fun onDestroy() {
    job.cancel() // cancel the Job
    super.onDestroy()
}

Now, just use the launch like below:

launch {
    val userOne = async(Dispatchers.IO) { fetchFirstUser() }
    val userTwo = async(Dispatchers.IO) { fetchSeconeUser() }
    showUsers(userOne.await(), userTwo.await())
}

As soon as the activity is destroyed, the task will get cancelled if it is running because we have defined the scope.

When we need the global scope which is our application scope, not the activity scope, we can use the GlobalScope as below:

GlobalScope.launch(Dispatchers.Main) {
    val userOne = async(Dispatchers.IO) { fetchFirstUser() }
    val userTwo = async(Dispatchers.IO) { fetchSeconeUser() }
}

Here, even if the activity gets destroyed, the fetchUser functions will continue running as we have used the GlobalScope.

This is how the Scopes in Kotlin Coroutines are very useful.

Exception Handling in Kotlin Coroutines

Exception handling is another important topic. We must learn this.

When Using launch

For this we need to create an exception handler like below:

val handler = CoroutineExceptionHandler { _, exception ->
    Log.d(TAG, "$exception handled !")
}

Then, we can attach the handler like below:

GlobalScope.launch(Dispatchers.IO + handler) {
    fetchUserAndSaveInDatabase() // do on IO thread
}

If there is any exception in fetchUserAndSaveInDatabase, it will be handled by the handler which we have attached.

When using in the activity scope, we can attach the exception in our coroutineContext as below:

class MainActivity : AppCompatActivity(), CoroutineScope {

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job + handler

    private lateinit var job: Job

}

And use like below:

launch {
    fetchUserAndSaveInDatabase()
}

When Using async

When using async, we need to use the try-catch block to handle the exception like below.

val deferredUser = GlobalScope.async {
    fetchUser()
}
try {
    val user = deferredUser.await()
} catch (exception: Exception) {
    Log.d(TAG, "$exception handled !")
}

This is how the exception handling can be done in the Kotlin Coroutines.

I think we have received a good amount of knowledge today. Thank you so much for your time.

Now, let's start using the Kotlin Coroutines.

Do share this blog with your fellow developers to spread the knowledge.

Apply Now: MindOrks Android Online Course and Learn Advanced Android

Happy Learning :)

Team MindOrks

Also, Let’s become friends on Twitter, Linkedin, Github, Quora, and Facebook.