Advanced Data Binding: Binding to LiveData (One- and Two-Way Binding)

Anastasia Finogenova
ProAndroidDev
Published in
5 min readJan 15, 2019

--

Data Binding Library gains more and more popularity in production of Android applications as it amplifies the advantages of the MVVM architecture. It allows you to bind UI components in your layouts to data sources in your app using a declarative format rather than programmatically. Wouldn’t it be nice to automatically notify the UI about changes in the data and get changes in the UI attributes propagated back? So let’s see how we can bind to LiveData with one- and two-way binding!

Why binding to LiveData?

The advantages of using a lifecycle aware components such as LiveData include:

  • No crashes due to stopped activities. 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.
  • Proper configuration changes. If an activity or fragment is recreated due to a configuration change (for instance, device rotation), it immediately receives the latest available data.
  • No memory leaks. No need to dispose subscriptions manually. The observers clean up after themselves when their associated lifecycle is destroyed.

LiveData VS ObservableFields

Unlike objects that implement Observable — such as observable fieldsLiveData objects know about the lifecycle of the observers subscribed to the data changes. Both LiveData and ObservableFields observe the changes, however the advantages of one over the other that I can point out are:

  • No manual lifecycle handling. UI components just observe relevant data and don’t stop or resume observation. LiveData automatically manages all of this since it’s aware of the relevant lifecycle status changes while observing.
  • More functionality with Transformations and MediatorLiveData. Using LiveData will allow you to benefit from the power of Transformations and also add multiple sources to MediatorLiveData. So if you have 5 EditText views in your layout, you don’t need to observe all 5 from your Activity or Fragment. You can observe only one MediatorLiveData which will save you some lines of codes and logic complexity.
  • Sharing resources. Creating custom objects extending LiveData will allow you to connect to the system service once, then any observer that needs the resource can just watch the object.

Starting using LiveData with Data Binding

To use a LiveData object with your binding class, you need to specify a lifecycle owner to define the scope of the LiveData object. The following example shows how to set the activity as the lifecycle owner after the binding class has been instantiated:

// Inflate view and obtain an instance of the binding class.
val binding: MainBinding = DataBindingUtil.setContentView(this, R.layout.main)
// Specify the current activity as the lifecycle owner.
binding.setLifecycleOwner(this)

So now we can use LiveData objects in the layout file main.xml as follows and the value of commentText will be set to the text attribute:

<android.support.design.widget.TextInputEditText
android:text="@{viewModel.commentText}" />

In some cases a warning “ ...is a boxed field but needs to be un-boxed to execute…” can appear when using LiveData in databinding. This indicates of using nullable types as LiveData object value. To suppress the warning it is recommended to either use a primitive (ObservableInt instead of MutableLiveData<Integer>) or use safeUnbox as follows:

android:text="@{safeUnbox(viewModel.commentText)}"

Implementing Two-Way Binding

Two-way binding becomes real handy in the cases when LiveData value is expected to be updated from the UI. When it is accessed in code, we would like to receive the updated value. To be able to do that we will add “=” before the curly braces of the binding expression:

<android.support.design.widget.TextInputEditText
android:text="@={viewModel.commentText}" />

Now whenever a user types new text into the view on the screen, the LiveData object will be updated and when accessing its value we will receive the latest update.

Creating a custom Binding Adapter

To take it further, let’s think of a less generic case. Imagine we would like to set the current tab in a ViewPager through data binding with the use of a LiveData object. For that we will need to create a custom attribute currentTab for ViewPager with the help of BindingAdapter:

companion object {
@BindingAdapter("currentTab")

@JvmStatic
fun setNewTab(pager: ViewPager, newTab: MutableLiveData<Int>) {
newTab.value?.let {
//don't forget to break possible infinite loops!
if (pager.currentItem != newTab.value) {
pager.setCurrentItem(newTab.value, true)
}
}
}
}

So now we can add the new attribute to the layout file and set the current item for ViewPager from the LiveData object value:

<android.support.v4.view.ViewPager
app:currentTab="@{viewModel.pagerCurrentTab}"/>

When the new value is set to pagerCurrentTab object, the code in the body of the BindingAdapter will be executed.

Using Two-Way Binding With Custom Attribute

Now, when updating the value in the LivaData object we created, ViewPager scrolls to the new position. That’s nice except for the fact that in our use case, the user also interacts with the UI and changes ViewPager’s position but the LiveData object still holds the “old” value. We would like to be notified of the changes to this attribute in order to implement some logic based on it or just check the current value. This could be achieved with the implementation of two-way binding.

We will require the following change to the layout file:

<android.support.v4.view.ViewPager
app:currentTab="@={viewModel.pagerCurrentTab}"/>

Another alteration will be creating an InverseBindingAdapter in addition to the BindingAdapter we already have. At this point, data binding knows what to do when the data changes (it calls the method annotated with @BindingAdapter) and what to call when the view attribute changes (it calls the InverseBindingListener). So now if user swipes ViewPager tabs, the LiveData object will be updated with the new value. However, for it to know when or how the attribute changes we have introduced a custom event. The naming of the event defaults to the attribute name with the suffix “AttrChanged”. In our case it is currentTabAttrChanged.

companion object {
@BindingAdapter("currentTab")
@JvmStatic
fun setTab(pager: ViewPager, itemLiveData: MutableLiveData<Int>){
itemLiveData.value?.let {
//don't forget to break possible infinite loops!
if (pager.currentItem != itemLiveData.value) {
pager.setCurrentItem(itemLiveData.value, true)
}
}
}
@InverseBindingAdapter(attribute = "currentTab", event = "currentTabAttrChanged")
@JvmStatic
fun getTab(pager: ViewPager) = pager.currentItem
}

A Word of Warning

Be careful not to introduce infinite loops when using two-way data binding. When the user changes an attribute, the method annotated @InverseBindingAdapter is called. This, in turn, would call the method annotated @BindingAdapter, which would trigger another call to the method annotated @InverseBindingAdapter, and so on.

For this reason, it’s important to break possible infinite loops by comparing new and old values in the methods annotated @BindingAdapter.

Some Final Thoughts

Lifecycle of Android components is complex and can be a pain to manage manually as keeping the UI up-to-date with data sources, therefore introducing LiveData, is a huge step up in lifecycle management. Adding data binding into the project can make code more concise and reactive in a sense that changes in data sources can be propagated automatically to the UI taking into account the configuration and lifecycle state. However, you using Data Binding Library shouldn’t be limited by only setting a data model’s properties into text fields. Binding to LiveData with one- and two-way binding will allow you to make the most of the Observer pattern and lifecycle awareness.

--

--