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 CancellationException
s 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?
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)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With