ViewLifecycleLazy and other ways to avoid View memory leaks in Android Fragments
Yet another take on AutoClearedValue
Handling the lifecycle of Android Fragments is difficult. I already covered this topic in an article I wrote in 2017, before Google added a View-specific Lifecycle
to Fragments in an attempt to solve some issues.
Until now, the number one cause of memory leaks when using Fragments remains the same: not properly clearing all direct and indirect View references in onDestroyView()
. Tools like Leak Canary can help detecting some of these cases.
And of course, Jetpack Compose represents a big paradigm change which completely removes the need to keep View references but hey, we still have our legacy codebases to maintain!
I recently came across a Medium blog post from Gabor Varadi describing an issue he encountered with a custom Kotlin delegated property he created to simplify managing view bindings in Fragments.
His solution is based on a class named AutoClearedValue
from the architecture components samples. The purpose of AutoClearedValue
is to provide a delegate that will automatically clear a value tied to one or more Views when the Fragment View hierarchy gets destroyed, in order to avoid the aforementioned memory leaks. This is an elegant alternative to declaring a Fragment property as nullable and manually setting it to null in onDestroyView()
, which can be easily forgotten.
It turns out I had already come up with my own solution to do the exact same thing. And since my version is simpler and generates more optimized bytecode compared to AutoClearedValue
and what Mr Varadi published, I decided to share it with you.
I named this delegate ViewLifecycleLazy
. Like the name implies, the delegated property value is computed lazily and the Fragment’s current view lifecycle is observed in order to automatically clear the value when it moves to the DESTROYED
state.
This is how it’s used in Fragments, for example with View Binding:
class MyFragment : Fragment(R.layout.my_fragment) {
private val binding by viewLifecycleLazy(MyFragmentBinding::bind)
}
Why it’s simpler
The original code adds some unnecessary complexity by observing viewLifecycleOwnerLiveData
, then in turn registering a new lifecycle observer on the new viewLifecycleOwner
when it becomes available. It turns out this is not needed because the viewLifecycleOwner
lifecycle observer can just be registered lazily, at the same time the value is initialized.
Since the property is designed to be read only when a view hierarchy is available, attempting to read it too early or too late will automatically throw an IllegalStateException
when calling fragment.requireView()
or fragment.viewLifecycleOwner
: we don’t need to do that check ourselves.
Why it generates more optimized bytecode
The original code is a classic property delegate, an object implementing the ReadOnlyProperty
interface which includes this function:
abstract operator fun getValue(thisRef: T, property: KProperty<*>): V
As a result, the compiler needs to generate extra metadata in the form of one KProperty
object for each delegated property, even if this parameter is not actually used in the code of the delegate. For more details, check my 2017 blog post “Exploring Kotlin’s hidden costs — Part 3” which covers delegated properties.
Kotlin 1.4 added an optimization allowing to skip the generation of this metadata for delegated property operators declared as inline
and not using the KProperty
parameter. This includes the built-in operator of the standard Lazy
interface:
inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
By implementing Lazy
instead of ReadOnlyProperty
, no unnecessary metadata code will be generated.
Note: some may argue that this implementation does not strictly comply with the
Lazy
contract which states that once initialized, a property must stay initialized. If you think this is problematic, it’s possible to alter the above code to not implementLazy
and still benefit from the compiler optimization by adding a specialized inline delegated property operator for theViewLifecycleLazy
class. The operator will have to be imported forViewLifecycleLazy
to be recognized as a delegate.
Final advice: only use it if you have to
You should always try to architect your code in such a way that you don’t have to create properties in your Fragments to store references to Views in the first place.
The best way to do this is to always pass View references as function arguments instead of retrieving them through the Fragment, and register lifecycle-aware callbacks in onViewCreated()
to update the View.
For example:
class MyFragment : Fragment(R.layout.fragment_example) {
private val myViewModel: MyViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val viewHolder = ViewHolder(view)
initNavigationView(viewHolder.navigationView)
val adapter = MyAdapter()
viewHolder.recyclerView.adapter = adapter
myViewModel.result.observe(viewLifecycleOwner) { result ->
viewHolder.titleTextView.text = result.title
adapter.submitList(result.list)
}
}
private fun initNavigationView(navigationView: NavigationView) {
// TODO
}
}
As you can see in the above code, all view references like viewHolder
or adapter
are declared and used within the scope of the onViewCreated()
function so none have to be stored as properties in the Fragment.
In order to improve readability, the code that configures the navigation menu is isolated in its own function initNavigationView()
and the view references are passed as arguments.
Finally, the code which updates the View after its initialization is located inside a lifecycle-aware callback declared in onViewCreated()
, in this case in the form of a LiveData observer. It could also be a coroutine launched from a LifecycleScope.
There are rare cases where this technique can’t be used or would make the code too complex. For these cases, you can use ViewLifecycleLazy
.
Let’s conclude with an example from one of my own projects:
interface RecycledViewPoolProvider {
val recycledViewPool: RecyclerView.RecycledViewPool
}class ParentFragment : Fragment(R.layout.fragment_parent), RecycledViewPoolProvider {
override val recycledViewPool by viewLifecycleLazy {
RecyclerView.RecycledViewPool()
}
}
It illustrates how to share a single lazily-created RecycledViewPool
instance provided by a parent Fragment with multiple child Fragments hosting similarly-configured RecyclerViews. When the parent Fragment’s View hierarchy gets destroyed, the RecycledViewPool
instance will be cleared automatically. A ViewLifecycleLazy
delegate is a good option to expose a View reference outside of the Fragment class, but be careful to only read it when the Fragment has a view hierarchy.
I hope you enjoyed the quick read (compared to my previous articles) and learned a thing or two about Fragments and Kotlin delegated properties. Let’s continue the conversation in the comments section and on social networks.