Android Data Binding-How to use it with <include> tag using live data and view model

Introduction

This was introduced back in 2015. Those old expressions from xml were really kind of tricky. But as it turns out, data binding is pretty amazing. And you might have already gained sufficient knowledge of it.You can find more details about data binding here.

At the beginner level, one interesting fact about data binding is, we can select how much of it we wanted to use. Like, getting rid of FindViewById is quite good. We can also create our custom binding adapters to ease the development. More than this, two-way data binding is also a great way to achieve observability.

Why data binding to LiveData?

LiveData is a lifecycle aware component, hence it offers below advantages

  • Handles orientation changes properly -If an activity or fragment is recreated due to a configuration change, it immediately receives the latest available data.
  • Avoid memory leaks -Observers clean up after themselves when their associated lifecycle is destroyed.
  • Avoid crashes -If the observer’s lifecycle is inactive, such as when an activity is in the back stack, then it doesn’t receive any LiveData events.

But what if we need to use data binding with <include> tag..?

Let’s consider a problem statement,

Consider, we have a User data maintenance app, and we have one feature which is common for more than one screen e.g user filter. There can be many ways to achieve this, but we have to find the best approach.

Basic Approaches

  1. You can create a custom component for the filter UI which can be reusable.
  2. You can create a separate filter layout and include it in the multiple screens.

Let’s consider the first approach where we need to create a custom component for the filter UI and need to use it across the screens. But we still need to write down the same business logic at multiple places. e.g logic to handle the events, data handling once the filter applies. In this case creating a custom component will not be an efficient way if the UI is not that complex.

Second approach will be more efficient than the first one. Why ? Because if we can separate out the view as well as the data associated with that view then we can make this functionality more reusable. Also we can integrate it more easily with the help of xml.

Now to implement this with second approach, we need to make use of Android Jetpack library i.e Data Binding. Because it allows you to bind UI components in your layouts to data sources in your app using a declarative format rather than programmatically.

Let’s see how to implement this?

But before that, let’s see, how to use data binding in an included layout — We know that how to support data binding with a normal layout, but if anyone needs to use it with an included layout then how to achieve this with livedata and viewmodel -

The first step is, you should enable data binding from gradle scripts, by adding the below code inside the module level build.gradle file.

android {
buildFeatures {
dataBinding true
}
...
}

We can develop a user filter view as a reusable one, so that we can include it whenever needed. Consider UserMainActivity class which has UserViewModel associated with it and the user filter feature is our common view.

So our main layout i.e activity_user_main.xml looks like this;

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="userViewModel"
type="com.example.databinding.view.UserViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/viewUserFilter"
layout="@layout/view_user_filter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:onLeftArrowClick =
"@{userViewModel.onLeftArrowIconClickEvent}"
app:onApplyFilterButtonClick=
"@{userViewModel.onApplyFilterButtonClickEvent}"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Here view_user_filter.xml is a reusable one, so we can not make it tightly coupled, hence we need to design it in such a way that one can use it across the app. In view_user_filter.xml, we have one image view and one button and both of them need a click listener, which will perform differently on the different screens. So we will extract these 2 lambda functions and will pass them separately using a data binding mechanism.

One thing to note here is that we need to use the same name for the variable where we have used binding in the main activity. That mean’s app:onLeftArrowClick and app:onApplyFilterButtonClick should match the name of the variable which will be used in view_user_filter.xml.

So view_user_filter.xml looks like this;

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="onLeftArrowClick"
type="kotlin.jvm.functions.Function0&lt;kotlin.Unit&gt;" />
<variable
name="onApplyFilterButtonClick"
type="kotlin.jvm.functions.Function0&lt;kotlin.Unit&gt;" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/leftArrowIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> onLeftArrowClick.invoke()}"
android:src="@drawable/abc_vector_test"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/applyFilterButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> onApplyFilterButtonClick.invoke()}"
android:text="@string/apply_filter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/leftArrowIcon"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

And now our UserViewModel.kt file looks like this;

package com.example.databinding.viewclass UserViewModel : BaseViewModel() {    val onLeftArrowIconClickEvent: () -> Unit = {
performLeftArrowIconClick()
}
val onApplyFilterButtonClickEvent: () -> Unit = {
performApplyFilterButtonClick()
}
private fun performApplyFilterButtonClick() {
TODO("Not implemented")
}
private fun performLeftArrowIconClick() {
TODO("Not implemented")
}
}

Now we just need to do the important step i.e to use LiveData with Data Binding, we need to set lifecycleOwner to the binding and pass view model.

And now our UserMainActivity.kt file looks like this;

package com.example.userimport android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.example.databinding.databinding.ActivityUserMainBinding
import com.example.databinding.view.UserViewModel
class UserMainActivity : AppCompatActivity() {

private val userViewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

//need to bind the layout
val binding: ActivityUserMainBinding =
DataBindingUtil.setContentView(this, R.layout.activity_user_main)

binding.lifecycleOwner = this
binding.userViewModel = userViewModel
}
}

And we are done. This way we can easily include any layout with the help of data binding. You can find more about data binding with include tag in the developer.android documentation as well.

One important thing is that data binding does not support include as a direct child of a merge element. So we can not perform a similar implementation for the <merge> tag.

Improved way to make it more reusable

Now above code would be helpful only if we have fewer UI elements in the reusable layout but what if we have more UI elements and we have to play with large amounts of data.

In that case, you can create a separate data class of livedata variables which will handle everything related to included layout. e.g. UserData.kt

package com.example.databinding.dataimport androidx.lifecycle.LiveDatadata class UserData(
val userName: LiveData<String>,
val userSurname: LiveData<String>,
var onLeftArrowClickEventListener: () -> Unit = {},
var onApplyFilterButtonClickEventListener: () -> Unit = {}
) {
fun setUseData(name: String, surname: String, ) {
userName.value = name
userSurname.value = surname
}
fun setListeners(
leftArrowClickListener: () -> Unit,
applyFilterButtonClickListener: () -> Unit
) {
onLeftArrowClickEventListener = leftArrowClickListener
onApplyFilterButtonClickEventListener = applyFilterButtonClickListener
}
}

And then we need to pass this to the main layout as below;

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="userData"
type="com.example.databinding.data.UserData" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/viewUserFilter"
layout="@layout/view_user_filter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:userData="@{userData}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

And our view_user_filter.xml looks like this;

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data> <variable
name="userData"
type="com.example.databinding.data.UserData" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{userData.userName}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/surname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{userData.userSurname}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/name" />
<ImageView
android:id="@+id/leftArrowIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> userData.onLeftArrowClickEventListener.invoke()}"
android:src="@drawable/abc_vector_test"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/surname" />
<Button
android:id="@+id/applyFilterButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> userData.onApplyFilterButtonClickEventListener.invoke()}"
android:text="@string/apply_filter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/leftArrowIcon"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

That’s it. Now you just need to update the UserData class inside your view model and call the methods to update livedata and it is done. In this way, you can easily extract the common functionality and use it across the app.

Conclusion

If you are using a clean architecture specially with patterns like MVVM, then your code should not be very tightly coupled in between the main architectural layers. In such cases, separating the responsibilities of your code base is really very important. Reusing UI helps us build new functionalities faster and in an efficient way.

Hope you find this helpful.

Thanks!😀

Android Developer

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