I'm experimenting with handling exceptions in Kotlin coroutines on Android.
My use case is I want to do bunch of tasks on background(in async manner) and update multiple UI components on a single activity.
I've designed a BaseActivity
structure to implement CoroutineScope
so I can couple couroutines invoked with the lifecycle of activity.
Also, I've a Repository
class which handles network calls.
I've achieved running multiple tasks concurrently. I know if I use a single Job
object to cancel all coroutines on onDestroy()
of the activity and doing (launch
) multiple coroutines in activity, Exception in any single coroutine will cancel the Job
from its CoroutineContext
. And since Job
is attached to lifecycle of activity, it will cancel all other coroutines too.
I've tried using a CoroutineExceptionHandler
. It catches exception but cancels the Job
too. Which in result cancels all other coroutines.
What I want?
Job
object to attach with activity lifecycleAdding code below
class BaseActivity : AppCompatActivity(), CoroutineScope {
val job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
launch(coroutineContext) {
Log.i("GURU", "launch1 -> start")
val result1Deferred = async { Repository().getData(1) }
val result2Deferred = async { Repository().getData(2) }
Log.i("GURU", "awaited result1 = " + result1Deferred.await() + result2Deferred.await())
}
//If Exception is Thrown, Launch1 should still continue to complete
advancedLaunch(coroutineContext) {
Log.i("GURU", "launch2 -> start")
val result1Deferred = async { Repository().getData(3) }
val result2Deferred = async { Repository().getData(4) }
delay(200)
throw Exception("Exception from launch 2")
Log.i("GURU", "awaited result2 = " + result1Deferred.await() + result2Deferred.await())
}
}
fun CoroutineScope.advancedLaunch(context: CoroutineContext = EmptyCoroutineContext,
exceptionBlock: (Throwable) -> Unit = {Log.i("GURU", it.message)},
launchBlock: suspend CoroutineScope.() -> Unit) {
val exceptionHandler = CoroutineExceptionHandler { _, throwable -> exceptionBlock(throwable)}
launch(context + exceptionHandler) { launchBlock() }
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
Log.i("GURU", "job -> cancelled")
}
}
The Log Result of this is
I/GURU: launch1 -> start
I/GURU: launch2 -> start
I/GURU: getData -> start 1
I/GURU: getData -> start 2
I/GURU: getData -> start 4
I/GURU: getData -> start 3
I/GURU: Exception from launch 2
--------- beginning of crash
You might want to replace your Job
with SupervisorJob
.
It prevents exceptions from propagating "upwards" (one failed child will not cause entire job to fail), but still allows you to push cancellation "downwards" (to running children).