Viewbinding in Fragment causes KotlinNullPointerException running within lifecycle scoped coroutine

2.4k views Asked by At

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?

3

There are 3 answers

0
esentsov On BEST ANSWER

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.

4
hrach On

You are using lifecycleScope which is different to lifecycle of the fragment's view. So you have to use different scope viewLifecycleOwner.lifecycleScope.launch {}. Btw, the link to Google Docs exactly says so :)

1
Reza On

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.