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 Views 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 18.8 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.kts (Module :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.6.4")
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
.
.
}
Code language: Gradle (gradle)
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 and a Seekbar to specify how many coroutines will 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 62-1:
An Android Kotlin Coroutines Tutorial
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 all are selected, right-click over the top-most TextView, and select the Center -> Horizontally menu option. Right-click again, this time selecting the Chains -> Create Vertical Chain option.
Select the SeekBar and change the layout_width property to 0dp (match_constraint) before adding a 24dp margin on the left and right-hand sides, as shown in Figure 62-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, launched each time the button is clicked. In 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) {
}
})
}
.
.
Code language: Kotlin (kotlin)
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 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 must be implemented using the async builder. All of these actions will need to be performed within a coroutine scope which must be declared. Within the MainActivity.kt file, make the following changes:
.
.
import kotlinx.coroutines.*
.
.
class MainActivity : AppCompatActivity() {
private val coroutineScope = CoroutineScope(Dispatchers.Main)
.
.
private suspend fun performTaskAsync(tasknumber: Int): Deferred<String> =
coroutineScope.async(Dispatchers.Main) {
delay(5_000)
return@async "Finished Coroutine $tasknumber"
}
.
.
}
Code language: Kotlin (kotlin)
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 = performTaskAsync(it).await()
}
}
}
.
.
Code language: Kotlin (kotlin)
The method implements a loop to launch the requested number of coroutines. It 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 when a coroutine is launched until the maximum is reached. After each coroutine completes the 5-second delay, the status text will update until all ten 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, 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.
Code language: plaintext (plaintext)
Although the app continues to function, 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. However, when this message appears in your own apps, it may be a sign that too many coroutines are running or that the asynchronous workload is too heavy for the main thread. That being the case, a different dispatcher may need to be used, perhaps using the withContext builder.
Summary
Building on the information covered in A Guide to Kotlin Coroutines, this chapter has created 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.