ViewLifecycleLazy and other ways to avoid View memory leaks in Android Fragments

A leaky pipe symbolizing a memory leak
Are your Fragments leaking?

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 implement Lazy and still benefit from the compiler optimization by adding a specialized inline delegated property operator for the ViewLifecycleLazy class. The operator will have to be imported for ViewLifecycleLazy to be recognized as a delegate.

Final advice: only use it if you have to

A meme of Kayode Ewumi grinning and pointing to his temple, with the caption: Can’t get a memory leak if you don’t have a property

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.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Christophe Beyls

Christophe Beyls

2.4K Followers

Android developer from Belgium, blogging about advanced programming topics.