Some tips of Kotlin Coroutines for Android developers
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.
- 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.