Coroutines for Beginners

The goal of this article is to help a beginner understand Coroutines, their benefits and how to use them in day-to-day android development. It does not aim to provide textbook correct definitions or cover everything about Coroutines.

Introduction

Historically, in many commonly used programming languages, Concurrency (Where a piece of code starts running concurrently to the rest of the program and returns immediately so that the system can do other things) was achieved using one of the two approaches: Threading and Callbacks.

However, there are some drawbacks with these approaches.

Drawbacks of Threading:

  1. Threads are expensive to create. Threads require context-switches that are expensive

  2. The number of threads you can create is limited by the underlying Operating System

  3. Debugging threads and managing race-conditions often make the usage of threads hard.

Drawbacks of Callbacks:

  1. Nested callbacks make the code unreadable

  2. Error propagation and handling becomes complicated

Go to this link for further reading on why Threading and Callbacks are considered harmful : https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/

Coroutines:

Coroutines solve the drawbacks of Threading and Callbacks.

Consider a use case where you need to login, authenticate and fetch news to display in the user’s feed.

Coroutines can turn code that looks like this:

login(onResult = {
    fetchUserToken(onResult = { token ->
        fetchNews(token, onResult = { news ->
            displayNews(news)
        })
    })
})

Into this:

lifecycleScope.launch {
    login()
    val token = fetchUserToken()
    val news = fetchNews(token)
    displayNews(news)
}

Coroutines are non-blocking. They start running concurrently to the rest of the program, and then return immediately so that the parent can do other things.

CoroutineScope(Dispatchers.IO).launch {
    println("This is executed before the blocking suspend function call")
    fetchData()
    println("This is executed after the blocking suspend function call")
}

println("This is executed immediately”)


suspend fun fetchData() {
    // Simulate a blocking operation
    delay(5000L)
}

The output of the above code is:

This is executed immediately
This is executed before the blocking suspend function call
// After the delay of 5 seconds
This is executed after the blocking suspend function call

Note that even though we called the function fetchData() that executes blocking code, the control flow pauses(suspends) the execution at this point and continues executing the rest of the program outside this CoroutineScope without blocking. When fetchData() finishes its execution, control flow resumes at this point and executes the rest of the code within this CoroutineScope.

Never block the main thread:

Android’s Main thread is responsible for drawing 60-120 frames per second on the display. Therefore, performing blocking operations on it will cause the UI to lag and may cause ANRs (Application Not Responding).

Coroutines framework has features that enable us to move the execution of code that can block the Main thread to other threads in Android.

In the above example, the functions login(), fetchUserToken() and fetchNews() perform network operations that block the thread in which they are running. To transform these blocking functions into suspending functions that return immediately, all we need to do is mark them with the suspend modifier.

Marking a function with the suspend modifier ensures that these functions can only be invoked from a CoroutineScope. The functions themselves are responsible for moving the execution to an appropriate thread pool using withContext(Dispatcher)

Different Dispatchers in Android

Dispatchers specify thread pools that you can run your coroutines on. There are three Dispatchers widely used in Android Development.

  1. Dispatchers.Main- The Main Dispatcher uses the Android’s Main thread. It must only be used to run code that updates the UI. Running blocking code such as Network calls or heavy computations on this Dispatcher will cause UI to lag.
withContext(Dispatchers.Main).launch {
    textView.text = “This is running on Android’s Main thread”
}
  1. Dispatchers.IO- The IO Dispatcher specifies a thread pool optimised to run blocking I/O operations such as Network calls, reading from a file.

     withContext(Dispatchers.IO).launch {
         val news = fetchNewsFromNetwork()
     }
    
  2. Dispatchers.Default- The Default Dispatcher specifies a thread pool optimised to run CPU-intensive computations such as compressing a Bitmap.

     withContext(Dispatchers.Default).launch {
         val compressedBitmap = compress(bitmap)
     }
    

Coroutine scopes:

A Coroutine can only be executed within a CoroutineScope.

Android has first-party support for CoroutineScopes for all the entities that have a lifecyle.

NOTE: You must add the dependencies from here https://developer.android.com/topic/libraries/architecture/coroutines#dependencies to use these CoroutineScopes.

Two widely used CoroutineScopes on Android are viewModelScope and lifecycleScope

viewModelScope: Coroutines in this scope are tied to the lifecycle of the ViewModel. Coroutines are cancelled when the ViewModel is cleared.

Use this scope to run coroutines that are only valid until the user is on the screen that owns this ViewModel such as fetching data to show on the screen. Coroutines run in this scope survive configuration changes since ViewModel s survive configuration changes

viewModelScope.launch { 
    fetchData()
}

lifecycleScope: Coroutines in this scope are tied to the lifecycle of the Activity/Fragment. Coroutines are cancelled when the Activity/Fragment is destroyed.

Use this scope to run coroutines that are only valid until the user is on the Activity/Fragment and also must be reset during configuration changes such as playing animations, setting data to UI

Inside Activity:

lifecycleScope.launch { 
    playLoadingAnimation()
    textView.text = “Loading…”
}

Inside Fragment:

viewLifecycleOwner.lifecycleScope.launch { 
    playLoadingAnimation()
    textView.text = “Loading…”
}

GlobalScope: Coroutines in this scope are tied to the process lifecycle. Coroutines run as long as the process running your app is active.

Only use this scope to run coroutines that must finish execution even if the user leaves the screen or minimises the app.

GlobalScope.launch {
    updateDataOnServer()
}

CoroutineScope: If none of the built-in CoroutineScope s fit your use case, you can create your own CoroutineScope by passing a Dispatcher and/or a Job instance.

val customScope = CoroutineScope(Dispatchers.Default + Job())

customScope.launch {
    computeSizeOfUniverse()
}

Parallel Execution:

Consider a use case where you fetch stories and posts to display on the user’s feed. These can be considered two independent API calls and can be executed in parallel. You can achieve this by executing the two API calls in different coroutine scopes within the same parent scope.

viewModelScope.launch(Dispatchers.IO) {
    fetchStories()
}

viewModelScope.launch(Dispatchers.IO) {
    fetchPosts()
}

Further, consider that after the two API calls, you must combine this data to display it to the user. You can combine the data only after both the parallel API calls finish. You can achieve this by wrapping the API call with async {} and calling await() on it.

viewModelScope.launch {
    val stories = async { fetchStories() }
    val posts = async { fetchPosts() }
    processData(stories.await(), posts.await())
}

Switch between threads:

You can switch execution between threads by using the withContext() function

suspend fun showBitmap(bitmap: Bitmap) {
    withContext(Dispatchers.Default) {
        val compressedBitmap = compress(bitmap)
        withContext(Dispatchers.Main) {
            imageView.setImageBitmap(compressedBitmap)
        }
    }
}

Returning a value from a suspend function:

You can return values from suspend functions. However, note that these can only be used within a CoroutineScope.

suspend fun getProduct(a: Int, b: Int): Int {
    return withContext(Dispatchers.Default) {
         a * b
    }
}

viewModelScope.launch {
    val result = getProduct(4, 0)
}

Exception Handling:

Coroutines greatly simplify exception handling compared to other asynchronous programming techniques. You can use try-catch blocks to handle exceptions.

viewModelScope.launch {
    try {
        val result = divide(9,0)
        println(result)
    } catch (e: ArithmeticException) {
        println(e.message)
    }
}


suspend fun divide(a: Int, b: Int): Int {
    return withContext(Dispatchers.Default) {
        a/b
    }
}

When using async {}, the exceptions must be handled by the code that calls await()

val deferred = viewModelScope.async {
    divide(9,0)
}

viewModelScope.launch {
    try {
        deferred.await()
    } catch (e: ArithmeticException) {
        println(e.message)
    }
}

Things to Keep In Mind:

Following are some important things (best practices, if you will) to keep in mind while working with coroutines:

  1. It must be safe to call a suspend function from any thread (Dispatcher). The suspend function itself is responsible for moving the execution to the appropriate thread (Dispatcher) using withContext()

  2. Unhandled exceptions thrown in coroutines can make your app crash. If exceptions are likely to happen, catch them in the body of any coroutines created with a CoroutineScope.

Conclusion/ Further Reading:

That should be enough to get you started using Coroutines in production apps. If you’d like to know more about Coroutines, check out these resources: