View Binding in Android, allows us to seamlessly interact with the user interface by providing auto-generated classes for each XML layout (on a module by module basis).
Therefore, a routine task of setting text in a TextView can be transformed from this:
val textView = findViewById<TextView>(R.id.text_view)
textView.text = "Foo"
To this:
binding.textView.text = "Foo"
Of course, view binding requires some additional boilerplate code before we can use it. In the example above, we need to first initialise the binding
.
// Step 1: Declare the binding
private lateinit var binding: FragmentBuyBindingoverride fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) // Step 2: Initialise it
binding = FragmentBuyBinding.inflate(layoutInflater)
// Step 3: Access it
binding.textView.text = "Foo"
}
Doing this for each fragment in our project is cumbersome. So, can we do better? Is there a cleaner, more efficient way that decreases our boilerplate code? Yes! By leveraging the power of Delegates.
The article assumes you have already set up view binding in your project. If not, refer to this link to learn how to set it up.
What are Delegates?
Delegation is defined as the shifting of responsibility for some particular work from one person to another. Simply put, you can think of delegation as a process in which one object delegates (assigns/gives) a particular task to a helper object, called the delegate. It then becomes the responsibility of the delegate to execute the task and provide the result to our initial object. Kotlin inherently provides support for both class and property delegates. Since our binding
object is a Fragment property, we’ll be working with property delegates.
Property Delegation
With property delegation, our delegate is responsible for handling the get
and set
for the specified property. We can thus abstract our common logic behind the delegate and re-use it for similar objects which in our case is the set of classes, auto-generated by view binding.
A thorough, more descriptive explanation of delegates can be found here.
A Custom Property Delegate for Fragment View Binding
Without further ado, let’s start writing our view binding delegate.
Step 1: Create the delegate class
Create a new class FragmentViewBindingDelegate.kt
and implement the ReadOnlyProperty<Fragment, T>
interface. In case your property is mutable, you can use the ReadWriteProperty<Fragment, T>
interface but since our binding
is immutable, we only extend the read only interface. In addition, we require the binding class in our delegate constructor.
class FragmentViewBindingDelegate<T : ViewBinding>(
private val bindingClass: Class<T>
) : ReadOnlyProperty<Fragment, T> { }
Step 2: Implement the ReadOnlyProperty
interface
As soon as we implement the ReadOnlyProperty
interface, we are asked to implement the getValue
member function.
class FragmentViewBindingDelegate<T : ViewBinding>(
private val bindingClass: Class<T>
) : ReadOnlyProperty<Fragment, T> {
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
// TODO
}
}
The first param of getValue()
is thisRef
which represents the object that holds our property. It is of type Fragment
in our case. The type for thisRef
is governed by how we implement our interface. This is why we implemented ReadOnlyProperty<Fragment, T>
instead of ReadOnlyProperty<Any?, T>
.
Step 3: Implement getValue
getValue()
needs to return an instance of our binding
object. So we create a variable called binding
. In getValue()
we initialise binding
if it’s not initialised and return it. This ensures we only initialise binding once and subsequently re-use the same instance.
private var binding: T? = null
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
if (binding == null)
binding = bindingClass.getMethod("bind", View::class.java).invoke(null, thisRef.requireView()) as T
return binding!!
}
At this point, we have handled most of our boilerplate code and can initialise our binding
as:
val binding by FragmentViewBindingDelegate(FragmentBuyBinding::class.java)
However, there are a few problems
1. The syntax to initialise binding
can be improved
2. Our delegate is not lifecycle aware and thus, it will leak memory
Let’s resolve them. Read on.
Step 4: Clean our binding
init syntax
Our delegate requires the class of our generic Class<T>
to return the correct view binding instance. Another way to get this information is by using the reified
keyword in Kotlin. You can learn more about reified here.
Add this inline function outside your delegate class:
inline fun <reified T : ViewBinding> Fragment.viewBinding() = FragmentViewBindingDelegate(T::class.java)
Et voilà! You can now initialise your binding with a clean, concise syntax.
private val binding by viewBinding<FragmentBuyBinding>()
Step 5: Make FragmentViewBindingDelegate
lifecycle aware
To make our delegate lifecycle aware we need to pass an instance of our host fragment to it.
class FragmentViewBindingDelegate<T : ViewBinding>(
private val bindingClass: Class<T>,
private val fragment: Fragment
) : ReadOnlyProperty<Fragment, T> {
And, update our inline function.
inline fun <reified T : ViewBinding> Fragment.viewBinding() = FragmentViewBindingDelegate(T::class.java, this)
Finally, subscribe to the fragment’s lifecycle and set the binding
to null
once the fragment is destroyed ensuring our delegate does not leak any memory.
init {
fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observe(fragment) { _viewLifecycleOwner ->
_viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
binding = null
}
})
}
}
})
}
And we’re done!
Final Code
inline fun <reified T : ViewBinding> Fragment.viewBinding() = FragmentViewBindingDelegate(T::class.java, this)
class FragmentViewBindingDelegate<T : ViewBinding>(
private val bindingClass: Class<T>,
private val fragment: Fragment
) : ReadOnlyProperty<Fragment, T> {
private var binding: T? = null
init {
fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
binding = null
}
})
}
}
})
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
if (binding == null)
binding = bindingClass.getMethod("bind", View::class.java).invoke(null, thisRef.requireView()) as T
return binding!!
}
}
And in our fragment,
private val binding by viewBinding<FragmentBuyBinding>()