Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoid cancelling of parent job on exception on child coroutine

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?

  1. Be able to use single Job object to attach with activity lifecycle
  2. Exception in one coroutine shouldn't cancel other coroutines

Adding 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
like image 439
Gaurav Chauhan Avatar asked Jan 02 '23 15:01

Gaurav Chauhan


1 Answers

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).

like image 96
Pawel Avatar answered Jan 04 '23 04:01

Pawel