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?
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
}
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.
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
}
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
.
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.")
}
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.