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 3 of 4 of this article. Click here to view the first page.

CoroutineDispatchers

Dispatchers determine what thread or thread pool the coroutine uses for execution. The dispatcher can confine a coroutine to a specific thread. It can also dispatch it to a thread pool. Less commonly, it can allow a coroutine to run unconfined, without a specific threading rule, which can be unpredictable.

Here are some common dispatchers:

This lack of confinement may lead to a coroutine destined for background execution to run on the main thread, so use it sparingly.

  • Dispatchers.Main: This dispatcher confines coroutines to the main thread for UI-driven programs like Swing, JavaFX, or Android apps. It’s important to note that this dispatcher doesn’t work without adding an environment-specific Main dispatcher dependency in Gradle or Maven.
    Use Dispatchers.Main.immediate for optimum UI performance on updates.
  • Dispatchers.Default: This is the default dispatcher used by standard builders. It’s backed by a shared pool of JVM threads. Use this dispatcher for CPU intensive computations.
  • Dispatchers.IO: Use this dispatcher for I/O-intensive blocking tasks that uses a shared pool of threads.
  • Dispatchers.Unconfined: This dispatcher doesn’t confine coroutines to any specific thread. The coroutine starts the execution in the inherited CoroutineDispatcher that called it, but only until the first suspension point. After the suspension ends, it resumes in the thread that is fully determined by the suspending function that was invoked.

    This lack of confinement may lead to a coroutine destined for background execution to run on the main thread, so use it sparingly.

import kotlinx.coroutines.*

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking<Unit> {
  launch { //context of the parent, main runBlocking coroutine
    println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
  }
  launch(Dispatchers.Unconfined) { //not confined -- will inmediatly run in main thread but not after suspension
    println("Unconfined: I'm working in thread ${Thread.currentThread().name}")
    delay(100L) // delays (suspends) execution 100 ms
    println("Unconfined: I'm working in thread ${Thread.currentThread().name}")
  }
  launch(Dispatchers.Default) { //will get dispatched to DefaultDispatcher 
    println("Default: I'm working in thread ${Thread.currentThread().name}")
  }
  launch(newSingleThreadContext("MyOwnThread")) {// will get its own new thread
    println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
  }    
}

Open in Playground ->

The print order changes per execution in the playground.

You’ll see how each of the dispatchers prints its own context, its own thread. Furthermore, you can see how you can create your own single-threaded contexts if you need a specific thread for some coroutine.

Handling Exceptions

On the JVM, threads are at the core of the Kotlin coroutines machinery. The JVM has a well-defined way of dealing with terminating threads and uncaught exceptions.

If an uncaught exception occurs in a thread, the JVM will query the thread for an UncaughtExceptionHandler. The JVM then passes it the terminating thread and the uncaught exception. This is important because coroutines and Java concurrency deal with the same exception behavior.

Coroutine builders fall into two exception categories. The first propagates automatically, like the launch(), so if bad things happen, you’ll know soon enough. The second exposes exceptions for the user to handle, such as async(). They won’t propagate until you call await() to get the value.

In Android, builders that propagate exceptions also rely on Thread.UncaughtExceptionHandler. It installs as a global coroutine exception handler. However, coroutine builders allow the user to provide a CoroutineExceptionHandler to have more control over how you deal with exceptions.

import kotlinx.coroutines.*

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking<Unit> {
  // propagating exception to the default Thread.UncaughtExceptionHandler
  val job = GlobalScope.launch {
    throw AssertionError()
  }

  // blocks thread execution until coroutine completes
  job.join()

  // launches async coroutine but exception is not propagated until await is called
  val deferred = GlobalScope.async(Dispatchers.Default) {
    throw AssertionError()
  }

  //defines a specific handler
  val handler = CoroutineExceptionHandler { _, exception -> 
    println("We caught $exception") 
  }

  // propagating exception using a custom CoroutineExceptionHandler
  GlobalScope.launch(handler) {
    throw AssertionError()
  } 

  // This exception is finally propagated calling await and should be handled by user, eg. with try {} catch {}
  deferred.await() 
}

Open in Playground ->

You should see an error being caught immediately. After that, comment out the first throw clause. You should once again see an exception thrown, but this time from async(). If you comment out the await()CoroutineExceptionHandler catches the exception, and prints out which exception happened.

Knowing this, there are three ways to handle exceptions. The first is using try/catch within a launch(), when you don’t have a custom exception handler. The second is by wrapping await() calls in a try/catch block. The last one is to use an exception handler, to provide one place to catch exceptions.

Coding Time

You’re going to work on a modified version of the RWDC2018 app. The modified app only displays photos taken at RWDevCon 2018.

The app retrieves these photos by using background threads. You’re going to replace the background threads implementation with Kotlin Coroutines.

Downloading the Project

Download the starter and final projects by clicking the Download Materials button at the top or bottom of this tutorial. Then, import the starter project into Android Studio.

Build and run the app, and you’ll see the following:

Take a moment to familiarize yourself with the structure of the project.

Main Activity

Next, navigate to the PhotosRepository.kt in Android Studio. This class contains the thread code to download the banner and photos for the RecyclerView. The photos download in a background thread. You then store the results in LiveData using postValue(). postValue() updates the data on the main thread.


Photos Repository

Next, you’ll start modifying the project to use coroutines.

Adding Dependencies

First, you need to add the Kotlin Coroutine dependencies to the app module. Open build.gradle in the app module and add the following dependencies:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2'
  • kotlinx-coroutines-core: Provides the core primitives to work with coroutines, such as builders, dispatchers and suspend functions.
  • kotlinx-coroutines-android: Provides Dispatchers.Main context for Android applications.

Sync the project to download the dependencies.

Life-cycle Awareness

Now that you have the dependency for Kotlin coroutines in your project, you can start implementing them. You’ll begin with the end in mind. It sounds kind of apocalyptic, but it’s not! :]

You must prepare your code to clean up active coroutines before implementing them. You’ll provide a way to cancel any active coroutines if the user decides to rotate or background the app, triggering Fragment and Activity life-cycle.

You’ll extend these Android lifecycle events to Kotlin classes which will handle coroutines internally.