Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin: How to bypass CancellationException

I'm porting some old RxJava code to Coroutines. With RxJava I could do this in my activity:

someBgOperation()
.as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(MyActivity.this)))
.subscribe(
    MyActivity.this::onSuccess,
    MyActivity.this::onError
);

The autodispose library would cancel the Observable if the activity was being closed. In this case RxJava would not call the error handler, so it was possible to do UI-related operations in the error handler safely, such as showing a dialog.

Now in Kotlin we could have this equivalent code launched from lifecycleScope in the Activity, or in a viewModelScope if using ViewModel:

viewModelScope.launch {
    try {
        someBgOperation()
    } catch (e: Exception){
        //show dialog
    }
}

Both scopes are automatically cancelled when the activity closes, just what Autodispose does. But the catch block will execute not only with normal errors thrown by someBgOperation itself, but also with CancellationExceptions that are used by the coroutines library under the hood to handle cancellation. If I try to show a dialog there while the activity is being closed, I might get new exceptions. So I'm forced to do something like this:

viewModelScope.launch {
    try {
        someBgOperation()
    } catch (ce: CancellationException){
        //do nothing, activity is closing
    } catch (e: Exception){
        //show dialog
    }
}

This feels more verbose than the Rx version and it has an empty catch clause, which would show a warning in the lint output. In other cases where I do more things after the try-catch, I'm forced to return from the CancellationException catch to stay UI-safe (and those returns are tagged returns). I'm finding myself repeating this ugly template again and again.

Is there a better way of ignoring the CancellationException?

like image 924
Mister Smith Avatar asked Jun 05 '20 16:06

Mister Smith


Video Answer


2 Answers

I can propose two solutions. First of all, the additional catch(e: CancellationException) clause looks a bit verbose. You can simplify the code to:

viewModelScope.launch {
    try {
        someBgOperation()
    } catch (e: Exception) {
        if (e !is CancellationException) // show dialog
    }
}

On the other hand, you can use Kotlin Flow whose catch operator is designed to ignore cancellations exactly for this purpose. Since you are not actually will be sending any values over the flow, your should use Flow<Nothing>:

flow<Nothing> {
    someBgOperation()
}.catch { e ->
    // show dialog
}.launchIn(viewModelScope)
like image 163
Roman Elizarov Avatar answered Sep 20 '22 00:09

Roman Elizarov


Edit: revised, since CancellationExceptions should not be swallowed.

You could create a helper function that converts to a Result so you can handle only non-cancellation Exceptions:

public inline fun <T, R> T.runCatchingCancellable(block: T.() -> R): Result<R> {
    return try {
        Result.success(block())
    } catch (e: Throwable) {
        if (e is CancellationException) {
            throw e
        }
        Result.failure(e)
    }
}

Usage:

viewModelScope.launch {
    runCatchingCancellable {
        someBgOperation()
    }.onFailure { e ->
        //show dialog
    }
}

And this function can serve as a safe alternative to runCatching to use in cancellable coroutines.

like image 41
Tenfour04 Avatar answered Sep 22 '22 00:09

Tenfour04