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.