Some tips of Kotlin Coroutines for Android developers

Freddie Wang
5 min readNov 27, 2019

Kotlin Coroutines is very powerful for asynchronous programming which the developers can write the async functions in the synchronized way, Now the Kotlin Coroutines became the stable API after Kotlin 1.3. But even Kotlin Coroutines is so powerful it doesn’t mean that you can use it without any problem. We still need to use Kotlin Coroutines carefully in our project. Here are some tips for using Kotlin Coroutines.

Tip1: Never use GlobalScope to launch the coroutines

According to the official document,

Global scope is used to launch top-level coroutines which are operating on the whole application lifetime and are not cancelled prematurely. Application code usually should use application-defined CoroutineScope, using async or launch on the instance of GlobalScope is highly discouraged.

Instead, we should use Activity, Fragment scope like below.

class CoroutineActivity : AppCompatActivity() {    // MainScope() = ContextScope(SupervisorJob() + Dispatchers.Main)    private val scope: CoroutineScope = MainScope()    override fun onDestroy() {        super.onDestroy()        coroutineContext.cancelChildren()    }    fun loadSomething() = scope.launch {        // code    }}

see more explanation about the GlobalScope.

Tip2: Always use SupervisorJob on Android

If you want to run the background task which would throw the exceptions, you may write the function like below

val job: Job = Job()val scope = CoroutineScope(Dispatchers.Default + job)fun backgroundTask(): Deferred<String> = scope.async { … }fun loadData() = scope.launch {  try {    doWork().await()  } catch (e: Exception) { … }}

Unfortunately, it can’t catch the exception, so the app will crash. It is because the backgroundTask would create a child job and the failure of the child job leads to an immediate failure of its parent.

The solution is to use SupervisorJob .

val job: Job = SupervisorJob()val scope = CoroutineScope(Dispatchers.Default + job)fun backgroundTask(): Deferred<String> = scope.async { … }fun loadData() = scope.launch {  try {    backgroundTask().await()  } catch (e: Exception) { … }}

and it only works if you explicitly run the async on the coroutine scope with SupervisorJob. It will still crash the app if the async is launched in the scope of parent coroutine like below.

val job: Job = SupervisorJob()val scope = CoroutineScope(Dispatchers.Default + job)fun loadData() = scope.launch {  try {    async { … }.await() // this is running in the scope of parent.  } catch (e: Exception) { … } // Can’t catch the exception}

if you want to launch the async in the parent scope, you should also assign the coroutine context explicitly.

val job: Job = SupervisorJob()val scope = CoroutineScope(Dispatchers.Default + job)fun loadData() = scope.launch {  try {    async(scope.coroutineContext) { … }.await() 
// Now the context is SupervisorJob.
} catch (e: Exception) { … } // Can catch the exception now.}

Tip3: Assign the context explicitly for the suspend functions

If we want to make the function “suspendable”, we can just add the suspend for a function like below.

suspend fun doSomething(): Result {  // Running for long time  return result}

It seems fine, but sometimes it brings implicit errors especially on Android. For example:

suspend fun loadingSomething(): Result {  loadingView.show()  val result = withContext(Dispatchers.IO) {    doSomething()  }  loadingView.hide()  return result}

the loadingView.show() and loadingView.hide() must be running in main thread. Therefore it would crash the application like this

val scope = CoroutineScope(Dispatchers.Default + job)scope.launch {  loadingSomething() // oops, it would crash the app.}

So the better way is that we assign the explicit dispatcher for the suspend functions

suspend fun doSomething(): Result = withContext(Dispatchers.IO) {  // Running for long time  return@withContext result}suspend fun loadingSomething(): Result =
withContext(Dispatchers.Main) {
loadingView.show() val result = doSomething() loadingView.hide() return@withContext result}val scope = CoroutineScope(Dispatchers.Default + job)scope.launch { loadingSomething()
// safe, because loadingSomething would run in main thread.
}

So if we assign the dispatcher explicitly, the function is safe and looks concise.

Tip4: Do not cancel the scope job directly.

Assume that we have implemented some job manager.

class JobManager {
private val parentJob = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Default + job)

fun job1() = scope.launch { /* do work */ }

fun job2() = scope.launch { /* do work */ }

fun cancelAllJobs() {
parentJob.cancel()
}
}
fun main() {
val jobManager = JobManager()

jobManager.job1()
jobManager.job2()
jobManager.cancelAllJobs()
jobManager.job1() // can't run the job here
}

And you would get the error at second job1(). It is because that when we cancel the parentJob , we put the parentJob into COMPLETED state. The coroutines launched in the scope of completed job won’t be executed.

Instead, we should use cancelChildren function to cancel the jobs.

class JobManager {
private val parentJob = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Default + job)

fun job1() = scope.launch { /* do work */ }

fun job2() = scope.launch { /* do work */ }

fun cancelAllJobs() {
scope.coroutineContext.cancelChildren()
}
}
fun main() {
val jobManager = JobManager()

jobManager.job1()
jobManager.job2()
jobManager.cancelAllJobs()
jobManager.job1() // No problem now.
}

Tip5: Use Android ktx for coroutine scope.

Android KTX has provided some useful extensions for coroutines. It can support lifecycle automatically so we don’t need to do the same thing again. The coroutineContext in lifecycleScope is SupervisorJob() + DispatchersMain.immediate and is cancelled when the lifecycle ended.

But if we need to run the suspended functions in the lifecyleScope. We should still need to assign the context as Tips3 mentioned. Especially the suspended functions include network calls.

lifecycleScope.launch {
doSomething()
}
suspend fun doSomething = withContext(Dispatchers.IO) { // Network calls should not run in main thread}

Tip6: Use async.await() and withContext for different purpose.

If you want to wait for the result from other suspended functions, you have two choices.

  1. Use the async { }.await
val result = async { doSomething() }.await()

2. Use the withContext

val result = withContext(Dispatchers.IO) {  doSomething()}

So what is the difference between async.await and withContext??

Let’s check the source code.

public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T = suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
...}

By the implementation, the async returns a Deferred object and withContext return the value directly.

So the caller of withContext would suspend immediately and wait for the result. The caller of async WOULD NOT suspend immediately. Therefore we can have a conclusion

If you need to run the coroutines sequentially, use withContext

If you need to run the coroutines in parallel, use async

In fact, if you use async.await directly, IntelliJ IDEA would show a warning and suggest you use withContext directly.

Tip7: Use suspendCancellableCoroutine to wrap callback-based interfaces.

Before Kotlin Coroutines is out, there are many projects using the callbacks for async programming. If we want to use Kotlin Coroutines to call the async functions with callback, is there any way to support the callback-based API?

Yes, we can wrap the callback interface by suspendCancellableCoroutine. Here is the example for Android’s CameraDevice.StateCallback

suspend fun CameraManager.openCamera(cameraId: String): CameraDevice? =
suspendCancellableCoroutine { cont ->
val callback = object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
cont.resume(camera)
}

override fun onDisconnected(camera: CameraDevice) {
cont.resume(null)
}

override fun onError(camera: CameraDevice, error: Int) {
// assuming that we don't care about the error in this example
cont.resume(null)
}
}
openCamera(cameraId, callback, null)
}

This pattern is extremely useful for Android applications because there are many callback-based API in Android frameworks.

References:

--

--

Freddie Wang

I’m a software engineer from Taiwan and living in Japan now.