Kotlin Coroutines in Android — Part 5

Coroutine cancellation

Andrea Bresolin
Kin + Carta Created
6 min readApr 5, 2019

--

There are cases when we would like to stop the execution of a coroutine. In this part we’re going to explore the coroutine cancellation mechanism.

CancellationException

Remember our launch() and async() coroutine builders? launch() returns a Job, while async() returns a Deferred. Deferred is in fact a Job as well. A Job provides the cancel() method for us to cancel the coroutine it represents if we wish so.

When we call cancel(), the specific coroutine it refers to is cancelled and all its children coroutines as well. To achieve this, a special exception is thrown. It’s CancellationException.

By throwing an exception, the code execution is interrupted as it happens with any other exception. The main difference is that this special exception is not treated in the same way as the others when it comes to the global exception handling in Android. The app doesn’t crash if we don’t catch this exception. It simply serves as a way of cancelling all the coroutines, but it is swallowed when it reaches the top parent coroutine.

This is an example of cancellation of individual coroutines:

Cancellation through CoroutineContext

We might want to cancel all the coroutines in a hierarchy instead of a single coroutine. We can achieve this by invoking cancel() or cancelChildren() on the CoroutineContext available in a CoroutineScope. Remember that we have a CoroutineScope available within our ViewModel or Presenter because we implement that interface and a scope is available as well in each coroutine. The context can have an associated Job. If we call cancelChildren() on a CoroutineContext, all the child coroutines of the Job are cancelled, but not the Job itself so we can still use this context to start new coroutines later if we wish so. In case we call cancel() instead of cancelChildren(), also the Job itself is cancelled and the context can’t be used anymore to start coroutines.

So, within any CoroutineScope, we can make the following call to cancel all the child coroutines in the hierarchy:

And if we want to cancel also the root Job in the hierarchy:

Note that in case the context doesn’t have a Job associated with it, the above methods won’t have any effect because none of the coroutines created within the context will have a parent Job.

Cancellation points

One question that you might have is: at which points in the code can CancellationException be thrown? The answer is simple: the suspension points.

Every suspend function is a candidate for throwing CancellationException.

Let’s take async() as an example. When you use it to start a new coroutine, the exception can be thrown by the corresponding await() of its Deferred because that’s a suspend function, while async() is not a suspend function.

In case there’s any other suspend function within the coroutine before calling await(), then that suspend function is a candidate as well for throwing CancellationException.

Catching CancellationException

CancellationException is just an exception like any other, so you can catch it if you want. This can be done at any suspension point as we’ve just mentioned. You need anyway to keep in mind that if you just catch this exception and don’t rethrow it, you’re suppressing the cancellation of the parent coroutine. When a coroutine is cancelled, all its children are cancelled as well. If any child swallows CancellationException, then the cancellation can’t be propagated up to the parent.

In the following example, we catch CancellationException just to execute some actions when a coroutine has been cancelled, but we throw it again to avoid stopping the cancellation.

In the following example, we suppress the cancellation instead.

This highlights that in case you want to catch a generic Exception, you should remember to rethrow it in case its actual type is CancellationException otherwise you’ll also suppress the cancellation of coroutines.

Cancellation is cooperative

We’ve seen that suspend functions are candidates to throw CancellationException, but what does it take for a custom suspend function to be cancellable? By default, all the suspend functions available in the standard coroutines library are cancellable, but if we write our own, we need to make it cancellable ourselves because, in the coroutines world, cancellation is cooperative.

This is a custom suspend function that calls other suspend functions within its body:

In this case, assuming that both aSuspendFunction() and anotherSuspendFunction() are cancellable, both (1) and (2) are candidates to throw CancellationException so our custom suspend function is cancellable as well because it uses other cancellable functions within its body.

A different case is when our custom suspend function executes a long computation without ever calling other cancellable suspend functions within it:

This suspend function will not return if its parent coroutine is cancelled. It will simply complete the long computation and execute any other code after that before returning. To make it cancellable, we need to check the isActive property. This property is available in any CoroutineScope.

In this case, we periodically check if the coroutine is still active making it possible to cancel the computation at every iteration of the loop. In case the coroutine is cancelled, we avoid executing any other code after the long computation loop and immediately throw CancellationException to indicate that this unit of work has been cancelled.

Cancellation in Android

This series is about coroutines in Android, so let’s take a quick look at the cancellation specifically for Android.

Let’s assume that the architecture of our app is either MVP or MVVM. The Presenter and the ViewModel are the typical classes that will implement CoroutineScope because we want to follow their lifecycle to decide when it’s a good time to cancel all the pending asynchronous tasks. That CoroutineScope is the parent scope of all the coroutines started within their lifecycle. Within each CoroutineScope, we have a CoroutineContext and that’s what we need to cancel all the running coroutines in the scope.

Here is an example of a Presenter in MVP:

This can be our base Presenter that exposes a cancelCoroutines() method to cancel all the child coroutines within the scope. Why do we use cancelChildren() and not cancel()? Typically, with MVP, we want to cancel all the running asynchronous tasks every time we have the onStop() event in the Activity connected to the Presenter, but the instance of the Presenter is not necessarily destroyed before the app comes back into foreground at a later point. If we were calling cancel(), the Job associated to the Presenter’s CoroutineScope would not be reusable anymore to start new coroutines and we want to keep it usable to create new coroutines when the app comes back into foreground.

Here is an example of a ViewModel in MVVM:

With a ViewModel from the Android Architecture Components, we want to cancel all the coroutines when we have the onCleared() event. In this case we use cancelChildren() as in the Presenter, but we could also use cancel() because the onCleared() event is triggered when the ViewModel is going to be destroyed, so we would have a new instance of it with a new CoroutineScope and CoroutineContext in case the app comes back into foreground.

What’s next

We’ve seen that CancellationException is a special exception, but what about the others? How are exceptions propagated through coroutines? This will be the topic of the next part.

Get the source code

The source code for this series can be found on GitHub. It’s an example Android project that covers multiple cases. Download it and play with it.

Missed the other parts of this series?

If you’ve missed the other parts of the Kotlin Coroutines in Android series, take a look at the introduction and check the full list of topics.

--

--