An Android Kotlin Coroutines Tutorial

The previous chapter introduced the key concepts of performing asynchronous tasks within Android apps using Kotlin coroutines. This chapter will build on this knowledge to create an example app that launches thousands of coroutines at the touch of a button.

Creating the Coroutine Example Application

Select the New Project option from the welcome screen and, within the resulting new project dialog, choose the Empty Activity template before clicking on the Next button.

Enter CoroutineDemo into the Name field and specify com.ebookfrenzy.coroutinedemo as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo) and the Language menu to Kotlin. Migrate the project to view binding using the steps outlined in section “Migrating a Project to View Binding”.

Adding Coroutine Support to the Project

The current version of Android Studio does not automatically include support for coroutines in newly created projects. Before proceeding, therefore, edit the Gradle Scripts -> build.gradle (Module: CoroutineDemo.app) file and add the following lines to the dependencies section (noting, as always, that newer versions of the libraries may be available):

dependencies {
.
.
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
.
.
}

After making the change, click on the Sync Now link at the top of the editor panel to commit the changes.

Designing the User Interface

The user interface will consist of a button to launch coroutines together with a Seekbar to specify how many coroutines are to be launched asynchronously each time the button is clicked. As the coroutines execute, a TextView will update when individual coroutines start and end.

Begin by loading the activity_main.xml layout file and add the Button, TextView, and SeekBar objects so that the layout resembles that shown in Figure 64-1:

Figure 64-1

To implement the layout constraints shown above begin by clearing all constraints on the layout using the toolbar button. Shift-click on the four objects so that all are selected, right-click over the top-most TextView and select the Center -> Horizontally menu option. Right-click once again, this time selecting the Chains -> Create Vertical Chain option.

Select the SeekBar and change the layout_width property to 0dp (match_constraints) before adding a 24dp margin on the left and right-hand sides as shown in Figure 64-2:

Figure 64-2

Modify the onClick attribute for the Button to call a method named launchCoroutines and change the ids of the top-most TextView, the SeekBar and the lower TextView to countText, seekBar, and statusText respectively. Finally, change the text on the Button to read “Launch Coroutines” and extract the text to a string resource.

Implementing the SeekBar

The SeekBar controls the number of asynchronous coroutines, ranging from 1 to 2000, that are launched each time the button is clicked. Remaining within the activity_main.xml file, select the SeekBar and use the Attributes tool window to change the max property to 2000. Next, edit the MainActivity.kt file, add a variable in which to store the current slider setting and modify the onCreate() method to add a SeekBar listener:

.
.
import android.widget.SeekBar
.
. 
class MainActivity : AppCompatActivity() {
 
    private lateinit var binding: ActivityMainBinding
    private var count: Int = 1
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
 
        binding.seekBar.setOnSeekBarChangeListener(object :
            SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(seek: SeekBar,
                                           progress: Int, fromUser: Boolean) {
                count = progress
                binding.countText.text = "${count} coroutines"
            }
 
            override fun onStartTrackingTouch(seek: SeekBar) {
            }
 
            override fun onStopTrackingTouch(seek: SeekBar) {
            }
        })
    }
.
.

When the seekbar slides, the current value will be stored in the count variable and displayed on the countText view.

Adding the Suspend Function

When the user taps the button the app will need to launch the number of coroutines selected in the SeekBar. The launchCoroutines() onClick method will achieve this using the coroutine launch builder to execute a suspend function. Since the suspend function will return a status string to be displayed on the statusText TextView object, it will need to be implemented using the async builder. All of these actions will need to be performed within a coroutine scope which also needs to be declared. Within the MainActivity.kt file make the following changes:

.
.
import kotlinx.coroutines.*
.
.
class MainActivity : AppCompatActivity() {

    private val coroutineScope = CoroutineScope(Dispatchers.Main)
.
.
    suspend fun performTask(tasknumber: Int): Deferred<String> =
        coroutineScope.async(Dispatchers.Main) {
            delay(5_000)
            [email protected] "Finished Coroutine ${tasknumber}"
        }
.
.
}

Given that the function only performs a small task and involves changes to the user interface, the coroutine is executed using the Main dispatcher. It is passed the sequence number of the coroutine to be launched, delays for 5 seconds, and then returns a string indicating that the numbered coroutine has finished.

Implementing the launchCoroutines Method

The final task before testing the app is to add the launchCoroutines() method which is called when the Button object is clicked. This method should be added to the MainActivity.kt file as follows:

.
.
import android.view.View
.
.
    fun launchCoroutines(view: View) {
 
        (1..count).forEach {
            binding.statusText.text = "Started Coroutine ${it}"
            coroutineScope.launch(Dispatchers.Main) {
                binding.statusText.text = performTask(it).await()
            }
        }
    }
.
.

The method implements a loop to launch the requested number of coroutines and updates the status TextView each time a result is returned from a completed coroutine via an await() method call.

Testing the App

Build and run the app on a device or emulator and move the SeekBar to a low number (for example 10) before tapping the launch button. The status text will update each time a coroutine is launched until the maximum is reached. After each coroutine completes the 5-second delay the status text will update until all 10 have completed (in practice these status updates will happen so quickly that it will be difficult to see the status changes).

Repeat the process with the SeekBar set to 2000, this time sliding the seekbar back and forth as the coroutines run to verify that the main thread is still running and has not been blocked.

Finally, with the Logcat panel displayed, set the SeekBar to 2000 and repeatedly click on the launch button. After about 15 clicks the Logcat panel will begin displaying messages similar to the following:

I/Choreographer: Skipped 52 frames!  The application may be doing too much work on its main thread.

Although the app continues to function, clearly the volume of coroutines running within the app is beginning to overload the main thread. The fact that this only occurs when tens of thousands of coroutines are executing concurrently is a testament to the efficiency of Kotlin coroutines. When this message begins to appear in your own apps, however, it may be a sign either that too many coroutines are running, or the asynchronous workload being performed is too heavy for the main thread. That being the case, a different dispatcher may need to be used, perhaps by using the withContext builder.

Summary

Building on the information covered in An Introduction to Kotlin Coroutines, this chapter has stepped through the creation of an example app that demonstrates the use of Kotlin coroutines within an Android app. The example demonstrated the use of the Main dispatcher to launch thousands of asynchronous coroutines, including returning results.

An Introduction to Kotlin Coroutines

The previous chapter introduced the concepts of threading on Android and explained how the user interface of an app runs on the main thread. To avoid degrading or interrupting user interface responsiveness, it is important that time-consuming tasks not block the execution of the main thread. One option, as outlined in the previous chapter, is to run any such tasks on a background thread, thereby leaving the main thread to continue managing the user interface. This can be achieved either directly using thread handlers or by making use of the AsyncTask class.

Although AsyncTask and thread handlers provide a way to perform tasks on separate threads, it can be time-consuming to implement, and confusing to read and maintain the associated code in an app project. This approach is also not the most efficient solution when large numbers of threads are required by an app.

Fortunately, Kotlin provides a lightweight alternative in the form of Coroutines. In this chapter, we will introduce the basic concepts of Coroutines, including terminology such as dispatchers, coroutine scope, suspend functions, coroutine builders, and structured concurrency. The chapter will also introduce the basic concepts of channel-based communication between coroutines.

What are Coroutines?

Coroutines are blocks of code that execute asynchronously without blocking the thread from which they are launched. Coroutines can be implemented without having to worry about building complex AsyncTask implementations or directly managing multiple threads. Because of the way they are implemented, coroutines are much more efficient and less resource-intensive than using traditional multi-threading options. Coroutines also make for code that is much easier to write, understand and maintain since it allows code to be written sequentially without having to write callbacks to handle thread-related events and results.

Although a relatively recent addition to Kotlin, there is nothing new or innovative about coroutines. Coroutines in one form or another have existed in programming languages since the 1960s and are based on a model known as Communicating Sequential Processes (CSP). In fact, Kotlin still uses multi-threading behind the scenes, though it does so highly efficiently.

Threads vs Coroutines

A problem with threads is that they are a finite resource and expensive in terms of CPU capabilities and system overhead. In the background, a lot of work is involved in creating, scheduling, and destroying a thread. Although modern CPUs are able to run large numbers of threads, the actual number of threads that can be run in parallel at any one time is limited by the number of CPU cores (though newer CPUs have 8 cores, most Android devices contain CPUs with 4 cores). When more threads are required than there are CPU cores, the system has to perform thread scheduling to decide how the execution of these threads is to be shared between the available cores.

To avoid these overheads, instead of starting a new thread for each coroutine and then destroying it when the coroutine exits, Kotlin maintains a pool of active threads and manages how coroutines are assigned to those threads. When an active coroutine is suspended it is saved by the Kotlin runtime and another coroutine is resumed to take its place. When the coroutine is resumed, it is simply restored to an existing unoccupied thread within the pool to continue executing until it either completes or is suspended. Using this approach, a limited number of threads are used efficiently to execute asynchronous tasks with the potential to perform large numbers of concurrent tasks without the inherent performance degenerations that would occur using standard multithreading.

Coroutine Scope

All coroutines must run within a specific scope which allows them to be managed as groups instead of as individual coroutines. This is particularly important when canceling and cleaning up coroutines, for example when a Fragment or Activity is destroyed, and ensuring that coroutines do not “leak” (in other words continue running in the background when they are no longer needed by the app). By assigning coroutines to a scope they can, for example, all be canceled in bulk when they are no longer needed.

Kotlin and Android provide some built-in scopes as well as the option to create custom scopes using the CoroutineScope class. The built-in scopes can be summarized as follows:

  • GlobalScope – GlobalScope is used to launch top-level coroutines which are tied to the entire lifecycle of the application. Since this has the potential for coroutines in this scope to continue running when not needed (for example when an Activity exits) use of this scope is not recommended for use in Android applications. Coroutines running in GlobalScope are considered to be using unstructured concurrency.
  • ViewModelScope – Provided specifically for use in ViewModel instances when using the Jetpack architecture ViewModel component. Coroutines launched in this scope from within a ViewModel instance are automatically canceled by the Kotlin runtime system when the corresponding ViewModel instance is destroyed.
  • LifecycleScope – Every lifecycle owner has associated with it a LifecycleScope. This scope is canceled when the corresponding lifecycle owner is destroyed making it particularly useful for launching coroutines from within activities and fragments.

For all other requirements. A custom scope will most likely be used. The following code, for example, creates a custom scope named myCoroutineScope:

private val myCoroutineScope = CoroutineScope(Dispatchers.Main)

The coroutineScope declares the dispatcher that will be used to run coroutines (though this can be overridden) and must be referenced each time a coroutine is started if it is to be included within the scope. All of the running coroutines in a scope can be canceled via a call to the cancel() method of the scope instance:

myCoroutineScope.cancel()

Suspend Functions

A suspend function is a special type of Kotlin function that contains the code of a coroutine. It is declared using the Kotlin suspend keyword which indicates to Kotlin that the function can be paused and resumed later, allowing long-running computations to execute without blocking the main thread. The following is an example suspend function:

suspend fun mySlowTask() {
    // Perform long-running task here    
}

Coroutine Dispatchers

Kotlin maintains threads for different types of asynchronous activity and, when launching a coroutine, it will be necessary to select the appropriate dispatcher from the following options:

  • Dispatchers.Main – Runs the coroutine on the main thread and is suitable for coroutines that need to make changes to the UI and as a general-purpose option for performing lightweight tasks.
  • Dispatchers.IO – Recommended for coroutines that perform network, disk, or database operations.
  • Dispatchers.Default – Intended for CPU-intensive tasks such as sorting data or performing complex calculations.

The dispatcher is responsible for assigning coroutines to appropriate threads and suspending and resuming the coroutine during its lifecycle. In addition to the predefined dispatchers, it is also possible to create dispatchers for your own custom thread pools.

Coroutine Builders

The coroutine builders bring together all of the components covered so far and actually launch the coroutines so that they start executing. For this purpose, Kotlin provides the following six builders:

  • launch – Starts a coroutine without blocking the current thread and does not return a result to the caller. Use this builder when calling a suspend function from within a traditional function, and when the results of the coroutine do not need to be handled (sometimes referred to as “fire and forget” coroutines).
  • async – Starts a coroutine and allows the caller to wait for a result using the await() function without blocking the current thread. Use async when you have multiple coroutines that need to run in parallel. The async builder can only be used from within another suspend function.
  • withContext – Allows a coroutine to be launched in a different context from that used by the parent coroutine. A coroutine running using the Main context could, for example, launch a child coroutine in the Default context using this builder. The withContext builder also provides a useful alternative to async when returning results from a coroutine.
  • coroutineScope – The coroutineScope builder is ideal for situations where a suspend function launches multiple coroutines that will run in parallel and where some action needs to take place only when all the coroutines complete. If those coroutines are launched using the coroutineScope builder, the calling function will not return until all child coroutines have completed. When using coroutineScope, a failure in any of the coroutines will result in the cancellation all other coroutines.
  • supervisorScope – Similar to the coroutineScope outlined above, with the exception that a failure in one child does not result in cancellation of the other coroutines.
  • runBlocking – Starts a coroutine and blocks the current thread until the coroutine reaches completion. This is typically the exact opposite of what is wanted from coroutines but is useful for testing code and when integrating legacy code and libraries. Otherwise to be avoided.

Jobs

Each call to a coroutine builder such as launch or async returns a Job instance which can, in turn, be used to track and manage the lifecycle of the corresponding coroutine. Subsequent builder calls from within the coroutine create new Job instances which will become children of the immediate parent Job forming a parentchild relationship tree where canceling a parent Job will recursively cancel all its children. Canceling a child does not, however, cancel the parent, though an uncaught exception within a child created using the launch builder may result in the cancellation of the parent (this is not the case for children created using the async builder which encapsulates the exception in the result returned to the parent).

The status of a coroutine can be identified by accessing the isActive, isCompleted and isCancelled properties of the associated Job object. In addition to these properties, a number of methods are also available on a Job instance. A Job and all of its children may, for example, be canceled by calling the cancel() method of the Job object, while a call to the cancelChildren() method will cancel all child coroutines.

The join() method can be called to suspend the coroutine associated with the job until all of its child jobs have completed. To perform this task and cancel the Job once all child jobs have completed, simply call the cancelAndJoin() method.

This hierarchical Job structure together with coroutine scopes form the foundation of structured concurrency, the goal of which is to ensure that coroutines do not run longer than they are required without the need to manually keep references to each coroutine.

Coroutines – Suspending and Resuming

To gain a better understanding of coroutine suspension, it helps to see some examples of coroutines in action. To start with, let’s assume a simple Android app containing a button that, when clicked, calls a function named startTask(). It is the responsibility of this function to call a suspend function named performSlowTask() using the Main coroutine dispatcher. The code for this might read as follows:

private val myCoroutineScope = CoroutineScope(Dispatchers.Main)

fun startTask(view: View) {
    myCoroutineScope.launch(Dispatchers.Main) {
        performSlowTask()
    }
}

In the above code, a custom scope is declared and referenced in the call to the launch builder which, in turn, calls the performSlowTask() suspend function. Since startTask() is not a suspend function, the coroutine must be started using the launch builder instead of the async builder.

Next, we can declare the performSlowTask() suspend function as follows:

suspend fun performSlowTask() {
    Log.i(TAG, "performSlowTask before")
    delay(5_000) // simulates long-running task
    Log.i(TAG, "performSlowTask after")
}

As implemented, all the function does is output diagnostic messages before and after performing a 5-second delay, simulating a long-running task. While the 5-second delay is in effect, the user interface will continue to be responsive because the main thread is not being blocked. To understand why it helps to explore what is happening behind the scenes.

First, the startTask() function is executed and launches the performSlowTask() suspend function as a coroutine. This function then calls the Kotlin delay() function passing through a time value. In fact the built-in Kotlin delay() function is itself implemented as a suspend function so is also launched as a coroutine by the Kotlin runtime environment. The code execution has now reached what is referred to as a suspend point which will cause the performSlowTask() coroutine to be suspended while the delay coroutine is running. This frees up the thread on which performSlowTask() was running and returns control to the main thread so that the UI is unaffected.

Once the delay() function reaches completion, the suspended coroutine will be resumed and restored to a thread from the pool where it can display the Log message and return to the startTask() function.

When working with coroutines in Android Studio suspend points within the code editor are marked as shown in the figure below:

Figure 63-1

Returning Results from a Coroutine

The above example ran a suspend function as a coroutine but did not demonstrate how to return results. Suppose, however, that the performSlowTask() function is required to return a string value which is to be displayed to the user via a TextView object.

To do this, we need to rewrite the suspend function to return a Deferred object. A Deferred object is essentially a commitment to provide a value at some point in the future. By calling the await() function on the Deferred object, the Kotlin runtime will deliver the value when it is returned by the coroutine. The code in our startTask() function might, therefore, be rewritten as follows:

fun startTask(view: View) {

    coroutineScope.launch(Dispatchers.Main) {
        statusText.text = performSlowTask().await()
    }
}

The problem now is that we are having to use the launch builder to start the coroutine since startTask() is not a suspend function. As outlined earlier in this chapter, it is only possible to return results when using the async builder. To get around this, we have to adapt the suspend function to use the async builder to start another coroutine that returns a Deferred result:

suspend fun performSlowTask(): Deferred<String> =
    coroutineScope.async(Dispatchers.Default) {
        Log.i(TAG, "performSlowTask before")
        delay(5_000)
        Log.i(TAG, "performSlowTask after")
    [email protected] "Finished"
}

Now when the app runs, the “Finished” result string will be displayed on the TextView object when the performSlowTask() coroutine completes. Once again, the wait for the result will take place in the background without blocking the main thread.

Using withContext

As we have seen, coroutines are launched within a specified scope and using a specific dispatcher. By default, any child coroutines will inherit the same dispatcher as that used by the parent. Consider the following code designed to call multiple functions from within a suspend function:

fun startTask(view: View) {

    coroutineScope.launch(Dispatchers.Main) {
        performTasks()
    }
}
 
suspend fun performTasks() {
    performTask1()
    performTask2()
    performTask3()
}
 
suspend fun performTask1() {
    Log.i(TAG, "Task 1 ${Thread.currentThread().name}")
}
 
suspend fun performTask2() {
    Log.i(TAG, "Task 2 ${Thread.currentThread().name}")
}
 
suspend fun performTask3 () {
    Log.i(TAG, "Task 3 ${Thread.currentThread().name}")
}

Since the performTasks() function was launched using the Main dispatcher, all three of the functions will default to the main thread. To prove this, the functions have been written to output the name of the thread in which they are running. On execution, the Logcat panel will contain the following output:

Task 1 main
Task 2 main
Task 3 main

Imagine, however, that the performTask2() function performs some network-intensive operations more suited to the IO dispatcher. This can easily be achieved using the withContext launcher which allows the context of a coroutine to be changed while still staying in the same coroutine scope. The following change switches the performTask2() coroutine to an IO thread:

suspend fun performTasks() {
    performTask1()
    withContext(Dispatchers.IO) { performTask2() }
    performTask3()
}

When executed, the output will read as follows indicating that the Task 2 coroutine is no longer on the main thread:

Task 1 main
Task 2 DefaultDispatcher-worker-1
Task 3 main

The withContext builder also provides an interesting alternative to using the async builder and the Deferred object await() call when returning a result. Using withContext, the code from the previous section can be rewritten as follows:

fun startTask(view: View) {

    coroutineScope.launch(Dispatchers.Main) {
          statusText.text = performSlowTask()
    }
}
 
suspend fun performSlowTask(): String =
    withContext(Dispatchers.Main) {
        Log.i(TAG, "performSlowTask before")
        delay(5_000)
        Log.i(TAG, "performSlowTask after")
 
        [email protected] "Finished"
    }
}

Coroutine Channel Communication

Channels provide a simple way to implement communication between coroutines including streams of data. In the simplest form this involves the creation of a Channel instance and calling the send() method to send the data. Once sent, transmitted data can be received in another coroutine via a call to the receive() method of the same Channel instance.

The following code, for example, passes six integers from one coroutine to another:

import kotlinx.coroutines.channels.*
.
.
val channel = Channel<Int>()
 
suspend fun channelDemo() {
    coroutineScope.launch(Dispatchers.Main) { performTask1() }
    coroutineScope.launch(Dispatchers.Main) { performTask2() }
}
 
suspend fun performTask1() {
    (1..6).forEach {
        channel.send(it)
    }
}

suspend fun performTask2() {
    repeat(6) {
        Log.d(TAG, "Received: ${channel.recieve()}")
    }
}

When executed, the following logcat output will be generated:

Received: 1
Received: 2
Received: 3
Received: 4
Received: 5
Received: 6

Summary

Kotlin coroutines provide a simpler and more efficient approach to performing asynchronous tasks than that offered by traditional multi-threading. Coroutines allow asynchronous tasks to be implemented in a structured way without the need to implement the callbacks associated with typical thread-based tasks. This chapter has introduced the basic concepts of coroutines including jobs, scope, builders, suspend functions, structured concurrency, and channel-based communication.