Jason A. Donenfeld
09/30/2020, 1:01 PMlifecycleScope.launch {
try {
// do stuff
delay(5000)
// do stuff
} catch (_: Throwable) {
requireContext()
}
}
When the Fragment is destroyed (by, e.g., the user pressing the back button), the Fragment's coroutine scope will be properly cancelled, as you'd expect. This in turn raises a CancellationException inside the scope. This exception is caught by our catch block, above. requireContext() is then called, and throws a new exception, because the context has already been destroyed.
It seems like destruction of the context and such is happening in the wrong order. What should be happening instead is that context and binding and other various members that Fragment has should be set to null only after coroutines have exited.
The example is contrived, but imagine something a bit more sophisticated, like:
lifecycleScope.launch {
try {
binding!!.config = getConfigAsync()
} catch (_: Throwable) {
binding!!.config = null
}
}
Or, even more innocent looking:
lifecycleScope.launch {
try {
// do something expensive
} catch (_: Throwable) {
Toast.make(ctx, getString(R.string.error_string), LONG).show()
}
}
That getString will crash too.
You might respond that CancellationException should always be handled differently in every try-catch block ever for all coroutines. Maybe you're right. But that's a lot of refactoring and weirdness. And it seems like this would be better solved by just fixing the race of lifecycle shutdown vs context detachment.
Does that seem reasonable?Joost Klitsie
09/30/2020, 1:38 PMJason A. Donenfeld
09/30/2020, 1:39 PMJoost Klitsie
09/30/2020, 2:21 PMJoost Klitsie
09/30/2020, 2:24 PMJoost Klitsie
09/30/2020, 2:24 PMCasey Brooks
09/30/2020, 6:14 PMlifecycleScope
gets cancelled, the Fragment is already in a bad state, so anything that happens within the coroutine at that point can’t be guaranteed to work. The coroutine itself is asynchronous while the Fragment is synchronous, so you can’t be 100% sure UI operations done in the coroutine are going to be possible. You should either manually check for cancellation in that catch
, or use some mechanism for communicating with the UI that makes that check for you to add a layer of separation between the async coroutine and the UI (such as sending the UI request through a channel/Flow and the UI reading from the channel/Flow, or wrapping UI operations explicitly in withContext(Dispatchers.Main) { }
)Jason A. Donenfeld
09/30/2020, 11:04 PMAdam Powell
10/01/2020, 3:50 AMAdam Powell
10/01/2020, 3:51 AMAdam Powell
10/01/2020, 3:53 AMAdam Powell
10/01/2020, 4:02 AMAdam Powell
10/01/2020, 4:03 AMCasey Brooks
10/01/2020, 1:28 PM