Kotlin Coroutines on Android: Things I Wish I Knew at the Beginning

Nick Capurso
Capital One Tech
Published in
6 min readApr 22, 2018

Coroutines are a great new feature of Kotlin which allow you to write asynchronous code in a sequential fashion. As with any new asynchronous solution, there is a learning curve. But not as steep, in my opinion, as that of something like RxJava. However, like RxJava, coroutines have a number of little subtleties that you end up learning for yourself during development time, or tricks that you pick up from others.

Our team has been working with coroutines for a few months now and overall it has been a fantastic experience. I wanted to write down some things we learned along the way which, had we known at the beginning of our journey, would have led to us making some different architectural decisions and reduced some debugging headaches.

If you are picking up coroutines for the first time, I highly recommend thoroughly watching Roman Elizarov’s talks at KotlinConf 2017 and also reading through the main guide on the official GitHub page:

The GitHub coroutines guide has been updated multiple times since we started using them, and includes some of the items we’ve learned ourselves as well. Once you get a feel for coroutines, it’s also helpful to read through the issues list as the maintainers have been very active in responding (and adding) to them in a very educational manner.

The List

UPDATE: Many items in this list have changed or evolved since this article was written. Coroutines has come a long way since we initially integrated in pre-1.0 / experimental!

You Can Convert Executors Into CoroutineDispatchers

Executors are generally used as a way to manage various thread pools. If you are integrating coroutines into an existing app, you may want a way to reuse your existing pools. Luckily, the coroutines dependency adds a handy extension function on Executors for you to convert them to a CoroutineDispatcher:

val executorService = Executors.newFixedThreadPool(100)
val coroutineDispatcher = executorService.asCoroutineDispatcher()

launch (coroutineDispatcher) {
// ...
}

On Android, this is also really useful for converting the AsyncTask thread pool into a dispatcher to allow Espresso tests to wait on your coroutines:

launch (AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher()) {
// ...
}

I recommend keeping your preferred dispatcher in something like a Dagger graph so that you can swap them in and out depending on your situation.

You Can Create a “Root” Coroutine Parent

UPDATE: This is now replaced by scoping!

If you are used to RxJava and managing disposables through a single object, there’s also an equivalent in coroutines by creating a blank parent job and using it for a number of coroutines:

val rootParent = Job()

fun foo() {
launch(UI, parent = rootParent) {
// ...
}
}

fun bar() {
launch(CommonPool, parent = rootParent) {
// ...
}
}

fun destroy() { rootParent.cancel() }

When you want to cancel all the downstream children, you can just cancel your root parent and, assuming the children are all cooperatively cancelable, they will all clean themselves up.

The CommonPool Has a Very Limited Size

The CommonPool is currently the “default” dispatcher if you don’t specify one when starting a coroutine. It also wraps a thread pool of size numProcessors-1 (with a minimum of 1), as evidenced by the source code:

private fun createPlainPool(): ExecutorService {
val threadId = AtomicInteger()
return Executors.newFixedThreadPool(defaultParallelism()) {
Thread(it, "CommonPool-worker-${threadId.incrementAndGet()}").apply { isDaemon = true }
}
}

private fun defaultParallelism() = (Runtime.getRuntime().availableProcessors() - 1).coerceAtLeast(1)

Depending on what API levels your Android app supports, this can be a real problem. There are quite a few older device models which are single or dual core, which means the CommonPool is only going to be of size 1. This is also different if you’re used to the AsyncTask thread pool, which has a minimum of 2 threads.

Depending on what your app is doing with coroutines, this can lead to device-specific deadlock problems, which is a major headache to debug.

Luckily, the fix isn’t too bad at all — you can just create your own thread pool when your minimum thread count requirement isn’t met and keep it somewhere centralized. For example, the following will use the CommonPool, except on dual or single core devices, where a separate thread pool of size 2 will be used:

val backgroundPool: CoroutineDispatcher by lazy {
val numProcessors = Runtime.getRuntime().availableProcessors()
when {
numProcessors <= 2 -> newFixedThreadPoolContext(2, "background")
else -> CommonPool
}
}

You can also choose to ditch the CommonPool entirely and use your own at all times. As I mentioned previously, if you keep your preferred coroutine dispatcher somewhere centralized and inject it across your app, it becomes easy to switch out later.

Async Can “Swallow” Exceptions if You’re Not Careful

If an exception is thrown during an async block, the Exception is not actually thrown immediately. Instead, it will be thrown at the time you call await on the Deferred that is returned.

// This won't crash...
val deferred = async {
throw Exception("Something failed")
}

launch {
delay(5000)
// ... until 5 seconds have elapsed and we call await()
deferred.await()
}

If you forget to call await() or it’s in a code branch that is missed, then the exception is effectively swallowed (which may or may not be a bad thing).

The suggestion here is really not to use async unless your specific use case calls for it (or, at least, to be aware of this behavior). The nice thing about coroutines is that you can more-or-less write sequential-asynchronous code, so you usually won’t need to use Future-focused coding.

On the other hand, this may also be a feature that you’re interested in — deferring exception handling until a later time.

You Can Get a Reference to the Surrounding CoroutineContext Inside a Suspend Function

UPDATE: Thanks to Louis CAD for telling me this feature is now available in the Kotlin Standard Library as of 1.2.30!

You can now simply reference coroutineContext within a suspend function to get access to the context of the current coroutine.

Thanks to the note above, you can just update to Kotlin 1.2.30, but here’s the old solution below with the explanation of the problem, in case it helps anyone.

Cancelling coroutines cooperatively requires you to set up parent-child relationships between your coroutines:

launch {
launch (coroutineContext) {
// Now the inner coroutine will cancel with the outer coroutine
}
}

But what if you have a situation like this:

fun foo() {
launch {
bar()
}
}

suspend fun bar() {
launch {
// ...
}
}

Cancelling foo’s coroutine will not cancel bar’s. Instead, you’d have to actually pass the CoroutineContext or the parent-to-use as a parameter, which isn’t great:

fun foo() {
launch {
bar(coroutineContext)
}
}

suspend fun bar(coroutineContext: CoroutineContext) {
launch(coroutineContext) {
// ...
}
}

However, there’s currently an open issue to add support for easily grabbing the context from inside suspend functions:
https://youtrack.jetbrains.com/issue/KT-17609

For now, you can use the following workaround:

import kotlin.coroutines.experimental.intrinsics.suspendCoroutineOrReturnsuspend fun coroutineContext(): CoroutineContext =   
suspendCoroutineOrReturn { cont -> cont.context }

And use it like this:

suspend fun bar() {
val context = coroutineContext()
launch(context) {
// ...
}
}

This allows you to keep your parameter lists clean and still enables you to set up cancellation chains easily!

Coroutines are a great new feature of Kotlin to help you write asynchronous code easier than ever before. Our team has had a great experience with them and I hope that sharing some of the subtleties and tricks improves your experience with them too!

--

--