Kotlin Coroutines Basics - Simple Android App Demo
Vincent Tsen
Posted on April 15, 2022
This simple Android app demonstrates the basic Kotlin coroutines usages such as creating coroutines (launch and async), cancelling coroutines.
This is part of the Kotlin coroutines series:
- Part 1 - Kotlin Coroutines Basics - Simple Android App Demo
- Part 2 - kotlinx.coroutines.delay() vs Thread.sleep()
- Part 3 - GlobalScope vs viewModelScope vs lifecycleScope vs rememberCoroutineScope
- Part 4 - launchWhenCreated() vs launchWhenStarted() vs launchWhenResumed() vs repeatOnLifeCycle()
I created this simple Android app to help me to understand the basic usage of Kotlin coroutines. The app demonstrates how to create coroutines jobs and run them concurrently. It probably won't cover 100% of the use cases, maybe at least 90%?
The app also uses simple MVVM architecture (without the Model
to be exact).
App Overview
There are 3 buttons and 2 display texts (left and right) UI in this app.
- Launch and Async buttons update both left text and right text UI concurrently
- left text and right text are started at -1 as an invalid value.
- When Launch or Async button is clicked, coroutines are created to update left text UI from 0 → 9 and right text UI from 10 → 19
- If the Cancel button is clicked, both texts are stopped being updated and value is set to -1.
- If no Cancel button is clicked, both texts continue to be updated until the final value 9 and 19
There are 2 ways to create coroutines:
-
CoroutineScope.launch
-
CoroutineScope.async
CoroutineScope.launch
To create a coroutine, we need to create CoroutineScope
first. In ViewModel
, CorotineScope
is already created (i.e. viewModelScope
). So it is highly recommended to use it instead of creating it yourself. One benefit is, all the CoroutineScope
children will be automatically cancelled when ViewModel
is destroyed.
// Create new coroutine - current job is the parent job
currentJob = viewModelScope.launch {
// Create a first new sub-coroutine - job1 is the child job
val job1 = launch {
...
}
// Create a second new sub-coroutine - job2 is the child job
val job2 = launch {
...
}
// job1 and job2 are coroutines that run concurrently
// wait for both job1 and job2 to complete
job1.join()
job2.join()
...
}
CoroutineScpoe.launch
is a non-blocking function, and it returns Job
immediately. To achieve concurrency, we can call CoroutineScpoe.launch
multiple times within the same coroutine scope. job1
is responsible to update the left text UI and job2
is responsible to update the right text UI.
If you want to wait for the job to complete, you need to call the Job.join()
suspend function. This will wait until the job is completed before it moves to the next line.
CoroutineScope.async
For creating coroutine that we want to wait for it's returned value, we use CoroutineScope.async
.
Similar to CoroutineScpoe.launch
, CoroutineScope.async
is a non-blocking function. Instead of returning Job
, it returns Deferred<T>
. The last line in the async block
, is the return type T
. For example, getData()
returns Int
, thus, the T
is Int
type.
// Create new coroutine
viewModelScope.launch {
// Create a sub-coroutine with async
val deferred = async {
...
getData()
}
// wait for async to return it's value
data.value = deferred.await()
...
}
Instead of using Job.join()
, you call Deferred<T>.awailt()
to wait for the CoroutineScope.async
to finish and also return the value from getData()
.
CoroutineScope.withContext()
By default, the coroutines are run on main/UI thread. You should move the long-running tasks to different thread so that it doesn't block the main/UI thread.
To switch to a different thread, you specify CoroutineDispatcher
. Here are the common pre-defined CoroutineDispatcher
that we can use:
-
Dispatchers.Main
- main/UI thread -
Dispatchers.Default
- CPU operation thread -
Dispatchers.IO
- IO or network operation thread
To use your own thread, you can create a new thread / new CoroutineDispatcher
using newSingleThreadContext("MyOwnThread")
. Most of the time, the pre-defined CoroutineDispatcher
are enough.
When creating a coroutine either with launch
or async
, you can specify the CoroutineDispatcher
.
viewModelScope.launch {
// Create coroutine that runs on Dispatchers.Default thread
launch(Dispatchers.Default) {
loadData()
}
// Create coroutine that runs on Dispatchers.Default thread
async(Dispatchers.Default) {
loadData()
}
}
However, a better solution is to use CoroutineScope.withContext()
in the suspend function instead of specifying the CoroutineDispatcher
during coroutine creation. This is recommended because it makes the suspend function safe to be called from main/UI thread.
private suspend fun loadData() {
//Switches / moves the coroutine to different thread
withContext(Dispatchers.Default) {
...
}
}
Please note
CoroutineScope.withContext()
does NOT create a new coroutine. It moves the coroutines to a different thread.
Job.cancelAndJoin()
To cancel a coroutine job, we call Job.cancel()
and Job.join()
. Most of the time, you can just simply call Job.cancelAndJoin()
. Please note that Job.cancelAndJoin()
is a suspend function. So you need to call it inside the coroutine.
fun onCancelButtonClick() {
if (currentJob == null) return
viewModelScope.launch() {
currentJob!!.cancelAndJoin()
}
}
currentJob
is an existing coroutine job that was created before.
kotlinx.coroutines.yield()
One important thing to note is coroutine cancellation is cooperative. If a coroutine is non-cooperative cancellation, there is no way we can cancel it. The coroutine will continue to runs until it is complete although Job.cancel()
has been called.
To make a coroutine cancellation cooperative, you can use:
CoroutineScope.isActive
kotlinx.coroutines.yield()
CoroutineScope.isActive
required CoroutineScope
object to be called, and you need to add logic to exit the coroutine, thus it is less flexible. Since yield()
can be called in any suspend function, I personally prefer to use it.
Please note the
kotlinx.coroutines.delay()
also make the coroutine cancellation cooperative.
For example, if you have a long-running task like below, the coroutine will not honor any Job.cancel()
request.
private suspend fun simulateLongRunningTask() {
repeat(1_000_000) {
Thread.sleep(100)
}
}
To make it to accept the Job.cancel()
request, you just need to add yield()
.
private suspend fun simulateLongRunningTask() {
repeat(1_000_000) {
Thread.sleep(100)
yield()
}
}
kotlinx.coroutines.JobCancellationException
When a coroutine is cancellation is accepted, an kotlinx.coroutines.JobCancellationException
exception will be thrown. You can catch the exception and perform some clean up.
currentJob = viewModelScope.launch {
try {
val job1 = launch {
...
}
val job2 = launch {
...
}
job1.join()
job2.join()
} catch (e: Exception) {
// clean up here
currentJob = null
}
}
kotlinx.coroutines.coroutineContext
For debugging coroutine, logging is the easiest way. kotlinx.coroutines.coroutineContext
is very useful for logging. It provides the coroutine and thread information.
Please note that it is a suspend property which can only be called from the suspend function.
Example of Utils.log()
utility suspend function to wrap the .Log.d()
:
object Utils {
suspend fun log(tag: String, msg: String) {
Log.d(tag, "$coroutineContext: $msg")
}
}
//Usage
Utils.log("ViewModel", "======= Created launch coroutine - onButtonClick() =======")
Example of Logcat output:
D/ViewModel: [StandaloneCoroutine{Active}@5261b32, Dispatchers.Main.immediate]: ======= Created launch coroutine - onButtonClick() =======
Some Thoughts
So far, all my personal projects do not use all the coroutines use cases above. I only use CoroutineScope.launch
and CoroutineScope.withContext()
which is enough for me to accomplish what I want. I don't even need to cancel a coroutine, although I can if I want to, and the apps still work perfectly.
[Update: April 13, 2022]: I did use joinAll()
parallelize a few network calls instead of running the code in sequential. Example below:
private suspend fun fetchArticlesFeed() : List<ArticleFeed> = coroutineScope {
val results = mutableListOf<ArticleFeed>()
val jobs = mutableListOf<Job>()
for(url in urls) {
val job = launch {
val xmlString = webService.getXMlString(url)
val articleFeeds = FeedParser().parse(xmlString)
results.addAll(articleFeeds)
}
jobs.add(job)
}
jobs.joinAll()
return@coroutineScope results
}
Source Code
GitHub Repository: Demo_CoroutinesBasics
Originally published at https://vtsen.hashnode.dev.
Posted on April 15, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.