Auto Inflated-Cleared View Binding in Fragments Using Delegation

Rahmi Cemre Ünal
5 min readOct 2, 2022

--

First and foremost, this article does not aim to teach view binding installation and how it works. Also, some prior knowledge of the fragment lifecycle is needed to understand what is going on.

I will still try to explain them briefly but if you want to learn the details, you can review the official view binding and fragment lifecycle documents.

So, let’s get those parts out of the way.

View Binding

View binding is a feature that allows you to write code more easily that interacts with views. Once view binding is enabled in a module, it generates a binding class for each XML layout file present in that module. An instance of a binding class contains direct references to all views with an ID in the corresponding layout.

Imagine we need to access a text view and change its text.

val textView = findViewById<TextView>(R.id.text_view)
textView.text = "new text"

View Binding eliminates this boilerplate that has to be done every time to access a view in a type-safe manner.

binding.textView.text = "new text"

Here, binding.textView is a direct reference to the TextView with the “text_view” id in our XML layout.

Let’s take a look at the usage example from the official docs to achieve this result assuming we have a layout file named result_profile.xml:

Fragments outlive the views. This means that if we forget to clear the binding reference in onDestroyView, we will have a memory leak. Handling the binding in this way is error-prone because we have to remember to clear the binding reference for every fragment.

However, the issue isn’t specific to View Binding, it’s specific to usage that a larger scoped component (fragment) holds a reference to a smaller scoped component (binding).

We could actually just bind the view in onViewCreated and use the binding reference locally:

Of course, then it wouldn’t need to be cleared since the fragment doesn’t hold a reference to its view that prevents itself from being destroyed.

One downside of this approach is we have to pass the binding reference all over the place if we need to access it from multiple methods. (Also what if we need to access a view for some special handling in a default fragment lifecycle callback such as onResume?)

We could also use the base class approach to eliminate the possibility of the binding reference not being cleared and the hassle of binding the view manually every time. Although I do not see any problem using the abstract base class to handle this specific case, I think it is not the best design choice to provide an option that can lead to overloading the abstract class too much to avoid code duplication when a common task needs to be done.

Other people you work with may misread this approach and try to add every popular feature to this abstract class. We need to make sure that all the other classes that inherit the base class wants every strongly coupled behavior of it otherwise it can quickly become unmaintainable.

We can rely on inheritance when there is no other way, but in this case, we can achieve the same result more elegantly with the delegation pattern. If you want to learn more about the pros and cons of inheritance vs composition and why we are choosing delegation, you can take a look at this article.

View Binding Delegation For Fragments

Unfortunately, tracking fragment lifecycle with delegation is a complex task. It is not easy to catch the right moment to clear binding reference from outside of the fragment. We will explore how to overcome this problem, so buckle up!

One of the most popular approaches is inspired by a class named AutoClearedValue found in the architecture components samples. This post by Gabor Varadi explains in detail how it can be adapted for view binding. Mr. Varadi has an additional post on how to solve a problem encountered with this method.

Fragment and view lifecycles are very tricky so, it would be pretty useful to read these articles for a better understanding.

Let’s see what the final version looks like:

It’s very similar to Mr. Varadi’s solution, so we don’t need to echo his explanation since it is very detailed but let me try to summarize it.

viewLifecycleOwnerLiveDataObserver is the observer that is responsible for catching the moment that fragment view is destroyed so binding reference can be cleared after that.

//binding reference set to be null when view lifecycle is destroyed
viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
binding = null
}
})

An observer needs to be added to the fragment lifecycle so it can start tracking the view lifecycle as implemented. Of course, when the fragment is destroyed, we should remove the observer to prevent memory leaks as well.

fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observeForever(
viewLifecycleOwnerLiveDataObserver
)
}

override fun onDestroy(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.removeObserver(
viewLifecycleOwnerLiveDataObserver
)
}
})

Also, I just want to highlight an addition that solves the problem I’ve come across in most solutions that observes the fragment view lifecycle with delegation.

val viewLifecycleOwner = it ?: run {
binding = null
return@Observer
}
//binding reference set to be null when view lifecycle is destroyed
viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
binding = null
}
})

the “run” block will be executed when fragment viewLifecycleOwner is null. If the fragment onDestroyView runs before the view lifecycle is initialized (for example when navigating in onViewCreated), the binding reference must be cleared here because the viewLifeCycleObserver will not be triggered. This way, I believe we have covered all the edge cases.

Finally, we can use it as a one-liner:

class ExampleFragment: Fragment(R.layout.example_fragment) {private val binding by viewBinding(ExampleFragmentBinding::bind)}

Thanks to Kotlin delegates and Jetpack Lifecycle components, there is no need to create an abstract base fragment to reduce the boilerplate of the view binding setup.

--

--