Kotlin Coroutines Basics - Simple Android App Demo

vtsen

Vincent Tsen

Posted on April 15, 2022

Kotlin Coroutines Basics - Simple Android App Demo

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:

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

Kotlin_Coroutines_Basics_Simple_Android_App_Demo_01.gif

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 09 and right text UI from 1019
  • 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()
    ... 
}
Enter fullscreen mode Exit fullscreen mode

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()
    ...
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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) {
        ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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)  
    }  
}
Enter fullscreen mode Exit fullscreen mode

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()
    }  
}
Enter fullscreen mode Exit fullscreen mode

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
    }   
}
Enter fullscreen mode Exit fullscreen mode

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() =======")
Enter fullscreen mode Exit fullscreen mode

Example of Logcat output:

D/ViewModel: [StandaloneCoroutine{Active}@5261b32, Dispatchers.Main.immediate]: ======= Created launch coroutine - onButtonClick() =======
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Source Code

GitHub Repository: Demo_CoroutinesBasics


Originally published at https://vtsen.hashnode.dev.

💖 💪 🙅 🚩
vtsen
Vincent Tsen

Posted on April 15, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related