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 betweenonCreate
andonDestroy
- but the
binding
is only valid betweenonViewCreated
andonDestroyView
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?