I am setting up my Fragment like suggested in Google docs as such:
private var _binding: MyBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = MyBinding.inflate(inflater, container, false)
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
Now I'm calling a coroutine that to my understanding should be scoped to the lifecycle of this fragment. It has a longer network call, then a success:
lifecycleScope.launch(Dispatchers.Main) {
when (myViewModel.loadFromNetwork(url)) {
true -> responseSuccess()
false -> responseFailure()
}
}
private suspend fun responseSuccess() {
binding.stateSuccess.visibility = View.VISIBLE
// ...
}
Now when I press the Android system-back button while loadFromNetwork
is still loading the fragment gets destroyed and onDestroyView()
is called. As such binding
now is null
. I'm getting a kotlin.KotlinNullPointerException
. What I not quite getting is why responseSuccess()
is still being executed even though I thought that lifecycleScope
is specifically meant for these kind of situations. According to Google Docs:
A LifecycleScope is defined for each Lifecycle object. Any coroutine launched in this scope is canceled when the Lifecycle is destroyed.
I understand this code can be fixed by a few changes and some manual null-checks, but I would like to understand how to fix this without boilerplate and in the intended manner. What is the purpose of using lifecycleScope to be lifecycle aware if not exactly this?
Coroutines cancellation is cooperative. It means it's a responsibility of a coroutine itself to check for cancellation. Most (or may be all) suspend operations in the coroutines library check for cancellation, but if you don't call any of them, you need to make your code cancellable as described here.
A better option to work with views in coroutines is to use lifecycle extensions which suspend / cancel the coroutine automatically when the lifecycle state is not in a required state.
Also please note, that cancellation is just a regular CancellationException
, so check you don't accidentally catch it.
well it may not be a very clean way of handling it but I suggest to cancel the job in onDestroyView
by your own self
define a job in class level like
lateinit var job:Job
then assign it like
job = lifecycleScope.launch(Dispatchers.Main) {
when (myViewModel.loadFromNetwork(url)) {
true -> responseSuccess()
false -> responseFailure()
}
and cancel it in onDestroView method before assigning null to _binding.
override fun onDestroyView() {
super.onDestroyView()
job.cancel()
_binding = null
}
the reason you get NULL POINTER EXCEPTION is that fragments have two lifecycles. 1)lifecycle and view lifecycle. _binding is assigned to null in onDestroyView but fragment lifecycle is still alive so coroutine's job is doing its work, when the network response arrives it run the launch block and wants to access binding object which is null by that time.
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