Mastering Kotlin Coroutines In Android - Step By Step Guide

undefined

I am writing this article to share my knowledge on 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 tutorial, 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.
  • Project to learn Kotlin Coroutines for Android by Examples.

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.

undefined

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 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 that 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 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 means that the coroutines don’t have their 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.

If you are preparing for your next Android Interview, Join our Android Professional Course to learn the latest in Android and land job at top tech companies.

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 fetchUser 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 the 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 help 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 coroutines

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 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.

In other words:

  • launch: fire and forget
  • async: perform a task and return a result

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

We have a function fetchUserAndSaveInDatabase like below:

suspend 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.Main) {
    fetchUserAndSaveInDatabase() // do on IO thread
}

As the fetchUserAndSaveInDatabase does not return anything, we can use the launch to complete that task and then do something on Main Thread.

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

We have two functions which return User like below:

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

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

Now, we can use the async like below:

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

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

We also have a video format for Launch vs Async. Check here.

If you want to check the complete implementation on "Parallel Multiple Network Calls Using Kotlin Coroutines", check this blog.

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 another way of writing the async where we do not have to write await().

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

But there are many more things that we should know about the withContext and the await.

Now, let's use withContext in our async example of fetchFirstUser and fetchSecondUser in parallel.

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

When we use withContext, it will run in series instead of parallel. That is a major difference.

The thumb-rules:

  • Use withContext when you do not need the parallel execution.
  • Use async only when you need the parallel execution.
  • Both withContext and async can be used to get the result which is not possible with the launch.
  • Use withContext to return the result of a single task.
  • Use async for results from multiple tasks that run in parallel.

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 situations.

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 onDestroy 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) { fetchSecondUser() }
    showUsers(userOne.await(), userTwo.await())
}

As soon as the activity is destroyed, the task will get canceled 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) { fetchSecondUser() }
}

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

One way is to use try-catch block:

GlobalScope.launch(Dispatchers.Main) {
    try {
        fetchUserAndSaveInDatabase() // do on IO thread and back to UI Thread
    } catch (exception: Exception) {
        Log.d(TAG, "$exception handled !")
    }
}

Another way is to use a handler:

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.Main + handler) {
    fetchUserAndSaveInDatabase() // do on IO thread and back to UI Thread
}

If there is an 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 !")
}

Now, let's see some more and the real use-cases of exception handling in Android Development.

Suppose, we have two network calls like below:

  • getUsers()
  • getMoreUsers()

And, we are making the network calls in series like below:

launch {
    try {
        val users = getUsers()
        val moreUsers = getMoreUsers()
    } catch (exception: Exception) {
        Log.d(TAG, "$exception handled !")
    }
}

If one of the network calls fail, it will directly go to the catch block.

But suppose, we want to return an empty list for the network call which has failed and continue with the response from the other network call. We can add the try-catch block to the individual network call like below:

launch {
    try {
        val users = try {
            getUsers()
        } catch (e: Exception) {
            emptyList<User>()
        }
        val moreUsers = try {
            getMoreUsers()
        } catch (e: Exception) {
            emptyList<User>()
        }
    } catch (exception: Exception) {
        Log.d(TAG, "$exception handled !")
    }
}

This way, if any error comes, it will continue with the empty list.

Now, what if we want to make the network calls in parallel. We can write the code like below using async.

launch {
    try {
        val usersDeferred = async {  getUsers() }
        val moreUsersDeferred = async { getMoreUsers() }
        val users = usersDeferred.await()
        val moreUsers = moreUsersDeferred.await()
    } catch (exception: Exception) {
        Log.d(TAG, "$exception handled !")
    }
}

Here, we will face one problem, if any network error comes, the application will crash!, it will NOT go to the catch block.

To solve this, we will have to use the coroutineScope like below:

launch {
    try {
        coroutineScope {
            val usersDeferred = async {  getUsers() }
            val moreUsersDeferred = async { getMoreUsers() }
            val users = usersDeferred.await()
            val moreUsers = moreUsersDeferred.await()
        }
    } catch (exception: Exception) {
        Log.d(TAG, "$exception handled !")
    }
}

Now, if any network error comes, it will go to the catch block.

But suppose again, we want to return an empty list for the network call which has failed and continue with the response from the other network call. We will have to use the supervisorScope and add the try-catch block to the individual network call like below:

launch {
    try {
        supervisorScope {
            val usersDeferred = async { getUsers() }
            val moreUsersDeferred = async { getMoreUsers() }
            val users = try {
                usersDeferred.await()
            } catch (e: Exception) {
                emptyList<User>()
            }
            val moreUsers = try {
                moreUsersDeferred.await()
            } catch (e: Exception) {
                emptyList<User>()
            }
        }
    } catch (exception: Exception) {
        Log.d(TAG, "$exception handled !")
    }
}

Again, this way, if any error comes, it will continue with the empty list.

This is how supervisorScope helps.

Conclusion:

  • While NOT using async, we can go ahead with the try-catch or the CoroutineExceptionHandler and achieve anything based on our use-cases.
  • While using async, in addition to try-catch, we have two options: coroutineScope and supervisorScope.
  • With async, use supervisorScope with the individual try-catch for each task in addition to the top-level try-catch, when you want to continue with other tasks if one or some of them have failed.
  • With async, use coroutineScope with the top-level try-catch, when you do NOT want to continue with other tasks if any of them have failed.
The major difference is that a coroutineScope will cancel whenever any of its children fail. If we want to continue with the other tasks even when one fails, we go with the supervisorScope. A supervisorScope won’t cancel other children when one of them fails.

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.

Learn Kotlin Coroutines for Android by Examples

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

If you are preparing for your next Android Interview, Join our Android Professional Course to learn the latest in Android and land job at top tech companies.

Happy Learning :)

Team MindOrks

Show your love by sharing this blog with your fellow developers.

Click here to share on Twitter.

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