An update to the FragmentViewBindingDelegate: the bug we’ve inherited from AutoClearedValue
Jetpack Lifecycle is quite powerful. We can extract logic, and make it lifecycle-aware. In fact, this allowed us in a previous article to simplify viewbinding and make it a single line.
However, I need to provide a quick heads-up: all is not as it seems.
The provided gists and code snippets assumed that the code in AutoClearedValue is always correct. In this article, I’ll show you how to break it.
The original source
The general idea behind AutoClearedValue is that a value hidden by this delegate would only be created when it is set (in our case, when it is accessed), and when viewLifecycleOwner
becomes DESTROYED
, it would null out the value. Makes sense, right?
In order to access the view lifecycle, the code waits until Fragment.onCreate()
, and registers the lifecycle observer there.
Then, using the fragment as the lifecycle owner (!), it attempts to register a lifecycle observer on the view lifecycle owner.
The bug hiding in plain sight
The problem is that it is possible for the view lifecycle to be initialized in such a way that fragment.onStart()
and therefore viewLifecycleOwner.lifecycle.addObserver
never actually happens. The fragment goes directly from onViewCreated
to onDestroyView
.
Meaning, fragment.onStart()
and fragment.onStop()
never happen.
This means that fragment.viewLifecycleOwner.observe(fragment)
will NEVER be ACTIVE
, and therefore the observe
block will never be called. The viewLifecycle reaches DESTROYED
without it being observed.
When does this happen?
If a fragment is added with commitNow()
, but then is detach()
ed. This would mean that this could happen with FragmentPagerAdapter
, as that also uses attach/detach
under the hood.
In my case, the bug surfaced when using the binding along with the following construct:
In case of a bottom navigation view, where fragments were added but attached/detached, the view in the binding
variable could get stuck: as it was never cleared out.
The above code should have worked out of the box, but apparently it reached a rather uncommon scenario: a fragment immediately destroying its view, without ever reaching onStart()
and onStop()
.
How to fix it?
As the problem is caused by using the fragment
as the lifecycle owner in order to observe the viewLifecycleOwnerLiveData
(as the view lifecycle owner can reach DESTROYED
without the fragment
ever reaching STARTED
), the solution is to not use the fragment as the lifecycle owner.
Using observeForever
instead skips the need for the LiveData to be activated by a LifecycleOwner.
If you were using the binding, you might want to update your binding accordingly.
Conclusion
While Jetpack Lifecycle itself seems reliable and powerful, the interactions between some of its moving elements — in this case, the Fragment lifecycle and the Fragment View lifecycle — can cause surprising results.
There’s a good chance that the room for error in this regard will be reduced when the view lifecycle becomes the same as the fragment lifecycle, and there will only ever be one lifecycle to worry about — although it’s a good question as to when this will happen (Fragment 1.4.0?), and what other effects it will have (will you need addToBackStack()
to keep a viewModelStore
alive while navigating? How will it change the behavior of setMaxLifecycle(CREATED)
?)
Either way, this is a bug that anyone who relied on the behavior of AutoClearedValue as is would have inherited themselves.
In a way, this is an interesting insight into how just like an Activity can go directly from onCreate
to onDestroy
, so can Fragments go directly from onViewCreated
to onDestroyView
.
You can check out the discussion thread on /r/androiddev.