Fragment.lifecycleScope and View Binding are not friends

When using view binding for fragments, one usually has a binding variable that references a view. So, it may seem like a good fit to use Fragment.lifecycleScope to execute async code that updates the view because that scope is automatically cancelled when the fragment is destroyed. Thus, no worrying about async code still being executed even though the view is gone and the fragment is destroyed.

However, there is a pitfall I didn’t notice until now, because it becomes a problem only in rare situations:

  • The lifecycleScope is alive between onCreate and onDestroy
  • but the binding is only valid between onViewCreated and onDestroyView in the Fragment’s lifecycle

So, the lifecycleScope survives the view being destroyed.
Example, modified from the Android documentation on view binding:

private var _binding: MyFragmentBinding? = null
private val binding get() = _binding!!

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    _binding = MyFragmentBinding.inflate(inflater, container, false)
    return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    lifecycleScope.launch {
        var i = 0
        while(true) {
            delay(1)
            binding.counterTextView.text = (i++).toString()
        }
    }
}

override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
}

This should be unsafe, because there can be a state where _binding is null but lifecycleScope is not cancelled.

Solutions

Using viewLifecycleOwner.lifecycleScope for async code that acts on the view instead of this.lifecycleScope is one solution. This scope has the same lifecycle as the view and thus as the view binding. Note that when a view is created, then destroyed, then created again, the viewLifecycleOwner will be a different instance as well.
I didn’t test this solution yet, but looking at the Android source code, it seems to do what it should.

Another solution is to deviate from the view binding example code from the Android documentation and not have this construct of a combination of a nullable _binding variable and a lateinit-alike not-nullable binding property that is not always valid but instead, simply have a

private var binding: MyFragmentBinding? = null

just like the Fragment.view property is also nullable. While this seems to be more clean from the Kotlin point of view (IMO), the downside of this is that you have to treat the view binding as possibly null everywhere in the fragment’s code, even though you know that in all the places that are not called asynchronously (which in my code will be usually over 90% of places) it can not be null.

Your opinions

Did you notice this issue in your app(s) as well? What are you opinions - what is the better solution? Or is there an even better solution?

Hmm, I realized that solution 2 (make binding nullable) can not alone be a solution. In the (rare) situation that the following is called:

  1. onCreateView
  2. onViewCreated
  3. onDestroyView
  4. onCreateView
  5. onViewCreated

which is possible according to the documentation, that counterTextView in the example would then be set by two async while loops that conflict each other because the lifecycleScope is not cancelled anywhere between step 1-6, so it just runs on.