Canceling Nested Kotlin Coroutines With CoroutineScope
Coroutines are one of the 😍 nicest language features in Kotlin. They provide a rather headache-free way to use the power of concurrency in your Kotlin program. I was very excited when I started playing around with them but eventually came across a problem: what if you have nested coroutines (like a parent-child coroutine).
For instance, I came across this problem when I wanted to launch
a coroutine and with-in of that coroutine I want to start multiple coroutines and when I cancel the parent coroutine the children needs to be canceled as well. How!
Gradle Stuff
Kotlin coroutine requires us to add some more stuff to our application modules build.gradle
file. We start by adding the following dependencies right after the android
section:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0-RC1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0-RC1'
In this article, I assume that you’ve some elementary proficiency with Kotlin coroutine. Now instead of directly jumping to above problem, I wanna start this article by just launching a simple coroutine and cancel it when the ViewModel is going to destroy.
class RepositoryViewModel constructor(private val movieRepo : MovieRepo) : ViewModel() { private var movieRepoJob : Job? = null fun getMovies() { movieRepoJob = launch { movieRepo.getPopularMovies() .... update view state } } override fun onCleared(){ movieRepoJob?.cancel() } }
You see that’s its a very simple example, we have a ViewModel and in that there’s a getMovies method who simply launch
a coroutine. Well, actually the launch
returns a Job
and that Job
allows you to cancel it while it’s been running. So, you can simply cancel the coroutine in the onCleared method of ViewModel. Simple isn’t it! 🤗
Parent-Child Async Tasks
Now let’s get back to our previous example where we have more than one async tasks running. We have one parent launch
block and inside that block, there’s another async
task is running. So, how do we keep track of those tasks effectively when our ViewModel is going off the screen or we need to cancel them. Using this solution our previous example would look like:
class MyViewModel constructor(private val apiService : ApiService) : ViewModel() { private var parentJob : Job? = null private var firstChildJob : Job? = null private var secondChildJob : Job? = null fun getMovies() { parentJob = launch { apiService.request1() firstChildJob = launch { apiService.request2() ........ } secondChildJob = launch { apiService.request3() ........ } } } override fun onCleared(){ parentJob?.cancel() firstChildJob?.cancel() secondChildJob?.cancel() } }
This above code works for our contrived example but its hackiness makes me go “ICK!”. Currently, we’ve three launch
and we have to keep track of three Jobs
and, maybe later we need to have four launch
and we need to keep track of four Jobs
. It gets little unwieldy and a bit too much code to actually maintain. Trying to make this style work in any nontrivial program would be a complete nightmare.
To make the above example work in a conical way we need to have something like CompositeDisposable
who can hold multiple other Disposable
and dispose of them if any error occur or canceled them all with just one method. Happily! the kotlin coroutine library gives us the CoroutineScope.
Kotlin CoroutineScope
A CoroutineScope
makes error handling easier, If any child coroutine fails, the entire scope fails and all of children coroutines are canceled. It should be implemented on entities with a well-defined lifecycle that is responsible for launching children coroutines. By entities, I mean Activity, Fragment, and ViewModel.
Every coroutine builder like launch
and async
are an extension of CoroutineScope and inherits corutineContext
to automatically propagate both context elements and cancellation.
There are two ways to use to CoroutineScope in an application.
- First, is to implement the CoroutineScope on entities.
- Second to use the coroutineScope top-level function which creates a new CoroutineScope and calls the specified
suspend
block with this scope.
1. Cancel Jobs By Implementing Kotlin CoroutineScope
Back to our previous parent-child coroutine example. Using CoroutineScope
our example code looks like this:
class MyViewModel constructor(private val apiService : ApiService) : ViewModel(), CoroutineScope { // 1 // 2 private val job = Job() // 3 override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main // 4 fun executeCalls() { launch(context = coroutineContext) { val firstRequestDeferred = async { apiService.request1() } val secondRequestDeffered = async { apiService.request2() } handleResponse(firstRequestDeferred,await(),secondRequestDeffered.await()) } } // 5 override fun onCleared(){ job.cancel() } }
Below is the explanation of the above code:
- As I said, we need to implement CoroutineScope so that our
launch
andasync
invoked in the scope of an entity. - We created this
Job
instance so that we can easily cancel allasync
call whenever our ViewModel is going to destroy. - We need to tell in which dispatchers our response is needed to dispatch.
- Note that an implementation of
launch
also defines an appropriatecoroutineContext
. Now the parent coroutine waits for all the coroutines inside their block to complete before completing themselves. If the ViewModel is going to destroy or any of the launched coroutines throws an exception then all the nested coroutines are canceled. - Cancel
Job
in ViewModel onCleared method. After destroying, all children jobs will be canceled automatically.
Note: You see with the above approach we don’t need to keep track so many Jobs
if we cancel the parent coroutine or any Exception
thrown inside any of the coroutines then all jobs will be canceled.
2. Cancel Jobs With CoroutineScope Top-level Function
// 1 suspend fun executeCall(apiService : ApiService) = couroutineScope { val firstDeferredRequest = async { apiService.request1() } val secondDeferredRequest = async { apiService.request2() } handleResponse(firstRequestDeferred.await(),secondDeferredRequest.await()) } // 2 val job = launch { executeCall() } // 3 job.cancel()
With structured concurrency, you have to wrap your code into a coroutineScope
block that establishes a boundary of your operation, its scope. All the async
coroutines become the children of this scope and, if the scope fails with an exception or is canceled, all the children are canceled, too.
If you wanna know how the kotlin suspended function works under the hood, do check out this article => Explore How Kotlin Coroutine Works Under The Hood.
That is all for now!
If you like this article, I appreciate your support to share this article with others to let the great Android and Kotlin community know. I’m just a beginner like you guys. I write what I learned. Please let me know what did I miss in this article.
Thank you for being here and keep reading…