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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Kotlin Coroutines Tutorial for Android : Advanced
30 mins
- What Are Coroutines?
- The Origins
- Nowadays
- Getting Started
- Key Components
- Suspendable Functions
- Continuations
- Coroutine Context
- Coroutine Builders
- Executing Concurrently
- Blocking Builder
- CoroutineScope
- Canceling a Job
- CoroutineDispatchers
- Handling Exceptions
- Coding Time
- Downloading the Project
- Adding Dependencies
- Life-cycle Awareness
- Updating the Repository
- Updating Injection Singleton
- Updating the PhotosFragment
- Registering the Lifecycle
- Main-Safe Design
- Defining CoroutineScope
- Hooking Into the Life-cycle
- Introducing Coroutines
- Where to Go From Here?
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}")
}
}
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()
}
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.
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.
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.