Kotlin Coroutines Tutorial for Android : Advanced

Gain a deeper understanding of Kotlin Coroutines in this Advanced tutorial for Android, by replacing common asynchronous programming methods, such as Thread, in an Android app. By Rod Biresch.

3.3 (4) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Coroutine Builders

To start and run new coroutines, you must use a Coroutine Builder. They take some code and wrap it in a coroutine, passing it to the system for execution. This makes them the bread and butter of coroutines.

The main builder for coroutines is launch(). It creates a new coroutine and launches it instantly by default. It builds and launches a coroutine in the context of some CoroutineScope:

GlobalScope.launch { // CoroutineScope
  // coroutine body
}

Once you get ahold of a CoroutineScope, you can use launch() on it, to start a coroutine. You can use coroutine builders in a normal non-suspending function, or other suspendable functions, which starts nested coroutines.

Executing Concurrently

Another coroutine builder is async(). It’s special because you can use it to return a value from a coroutine, doing so allows concurrent execution. You’d use async() from any coroutine, like so:

GlobalScope.launch { // CoroutineScope
  val someValue = async { getValue() } // value computed in a coroutine
}

However, you can’t use the value just yet. async() returns a Deferred which is a non-blocking cancellable future. To obtain the result you have to call await(). Once you start awaiting, you suspend the wrapping coroutine until you get the computed value.

Blocking Builder

You can use another builder for coroutines, which is a bit unconventional. runBlocking() forces coroutines to be blocking calls.

runBlocking is a builder that blocks the thread until the execution completes to avoid JVM shutdown in special situations like main functions or tests. You should avoid using it in regular Kotlin coroutine code.

To explain how to start and execute Kotlin coroutines, it’s best to take a look at some live snippets of code:

import kotlinx.coroutines.*
import java.lang.Thread

@OptIn(DelicateCoroutinesApi::class)
fun main() {
  GlobalScope.launch {  // launch new coroutine in background and continue
    delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
    println("World!") // print after delay
    val sum1 = async { // non blocking sum1
      delay(100L)
      2 + 2
    }
    val sum2 = async { // non blocking sum2
      delay(500L)
      3 + 3
    }
    println("waiting concurrent sums")
    val total = sum1.await() + sum2.await() // execution stops until both sums are calculated
    println("Total is: $total")
  }
  println("Hello,")     // main thread continues while coroutine executes
  Thread.sleep(2000L)   // block main thread for 2 seconds to keep JVM alive
}

Open in Playground ->

The snippet above launches a Kotlin coroutine which uses delay() to suspend the function for one second. Since Kotlin coroutines don’t block any threads, the code proceeds to the second println() statement and prints Hello,.

Next, the code sleeps the main thread, so the program doesn’t finish before the coroutine completes its execution. The coroutine runs its second line and prints World!.

It then concurrently builds and starts two async coroutines. Finally, when both concurrent operations are complete, it prints the total.

This is a simple but effective way to learn about Kotlin coroutines and the idea behind them.

Take a look at the return type of launch(). It returns a Job, which represents the piece of computation that you wrapped in a coroutine. You can nest jobs and create a child-parent hierarchy.

You’ll see how to use this to cancel coroutines in a later snippet.

One of the things you used above is the GlobalScope instance for the coroutine scope. Let’s see what scopes are and how you should approach them.

CoroutineScope

CoroutineScope confines new coroutines by providing a lifecycle-bound component that binds to a coroutine. Every coroutine builder is an extension function defined in the CoroutineScope type. launch() is an example of a coroutine builder.

You already used GlobalScope, typically, you’d use CoroutineScope over GlobalScope in an Android app to control when lifecycle events occur.

Note: You use GlobalScope to launch top-level coroutines that are not bound to any Job. Global scope operates on the application lifetime. It is easy to unknowingly create resources and memory leaks using GlobalScope therefore, it’s considered a “delicate” API and usage must be annotated appropriately.

In an Android app, you implement CoroutineScope on components with well-defined lifecycles. These components include Activity, Fragment and ViewModel.

Calling launch() on CoroutineScope provides a Job that encapsulates a block of code. Once the scope cancels, all the Kotlin coroutines within clear up their resources and cancel.

Take the following snippet of code:

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
  launch { 
    delay(200L)
    println("Task from runBlocking")
  }

  coroutineScope { // Creates a new coroutine scope
    val job = launch {
      println("Task from nested launch, this is printed")
      delay(500L) 
      println("Task from nested launch, this won't be printed")
    }

    delay(100L)
    println("Task from first coroutine scope") // Printed before initial launch
    job.cancel() // This cancels nested launch's execution
  }
    
  println("Coroutine scope is over") // This is not printed until nested launch completes/is cancelled
}

Open in Playground ->

Examining the snippet above, you’ll see a few things.

First, you force the coroutines to be blocking, so you don’t have to sleep the program as you did before. Then, you launch a new coroutine which has an initial delay. After that, you use coroutineScope() to create a new scope. You then launch a coroutine within it, saving the returned Job.

Because you delay the initial launch(), it doesn’t run until the coroutineScope() executes fully. However, within the coroutineScope(), you store and delay the Job and the nested coroutine. Since you cancel it after it delays, it’ll only print the first statement, ultimately canceling before the second print statement. And, as the coroutineScope() finishes, the initial launch() finishes its delay, and it can proceed with execution.

Finally, once the scope finishes, the runBlocking() can finish as well. This ends the program. It’s important to understand this flow of execution to build stable coroutines without race conditions or hanging resources.

Canceling a Job

In the previous section, you saw how to cancel the execution of a coroutine. You should understand that a Job is a cancellable component with a lifecycle.

Jobs are typically created by calling launch(). You can also create them using a constructor – Job(). They can live within the hierarchy of other jobs, either as the parent or a child. If you cancel a parent Job, you also cancel all its children.

If a child Job fails or cancels, then its parent and parent hierarchy will also cancel. The exception the hierarchy receives is, of course, a CancellationException.

Note: There is a special type of a Job which doesn’t cancel if one of its children fail – the SupervisorJob. You can check it out at the official documentation.

So, the failure of a child will, by default, cancel its parent and any other children in the hierarchy. Sometimes you need to wait until a coroutine execution is effectively canceled. In that case, you can call job.cancelAndJoin() instead of job.cancel().

import kotlinx.coroutines.*

fun main() = runBlocking {
  val startTime = System.currentTimeMillis()
  val job = launch(Dispatchers.Default) {
    var nextPrintTime = startTime
    var i = 0
    while (isActive) { // cancelable computation loop
      // print a message twice a second
      if (System.currentTimeMillis() >= nextPrintTime) {
        println("I'm sleeping ${i++} ...")
        nextPrintTime += 500L
      }
    }
  }
  delay(1300L) // delay a bit
  println("main: I'm tired of waiting!")
  job.cancelAndJoin() // cancels the job and waits for its completion
  println("main: Now I can quit.")    
}

Open in Playground ->

The output for the program will be a few prints from the while loop, following with the cancel and finally the main() finishing.

There are benefits to canceling a coroutine in an Android app. For example, say an app goes into the background and an Activity stops. In that case, you should cancel any long-running API calls to clean up resources. This will help you avoid possible memory leaks or unwanted behavior.

You can cancel a Job, along with any children, from an Activity event like onStop(). It’s even easier if you do it through the use of CoroutineScope, but you’ll do that later.