Search

[Updated] Two-Way Data Binding on Android: Observing Your View with XML

Andrew Bailey

13 min read

Feb 2, 2021

[Updated] Two-Way Data Binding on Android: Observing Your View with XML

This post has been updated from the original 2017 version. At a high level, the new post: Converts all the information to reflect the move to Kotlin, adds clarification to separate Data Binding from modern MVVM conventions, and includes disambiguation on view model vs. androidx‘s ViewModel.

Android Two-Way Data Binding (Updated)

If you’ve used Data Binding in an Android app before, you’ll know how it makes your life easier by simplifying the problems you face when building your UI. Not only does it give you a type-safe, compile-time verified replacement to the standard findViewById method, but can also take care of all the heavy lifting in keeping your views up-to-date by seamlessly integrating your Java/Kotlin code with your XML layouts. It can also serve as the glue between the View and your View Model if you are using the MVVM architecture pattern on Android. If you haven’t tried out Data Binding yet, you can read more about it in one of our other blog posts, or on Google’s Data Binding documentation.

Data Binding is built around the idea of using data from a regular Java/Kotlin object to set attributes on your layouts, and that’s the extent to which most people use it. This is great because it allows you to define your view logic independently from the Android Framework (a boon for unit testing), but what if your view needs to set attributes on your object?

This is where two-way Data Binding comes in. Two-way Data Binding is a technique of binding your objects to your XML layouts so that the layout can send data to your binding object. This is compared to a “traditional” or “one-way” Data Binding setup, where data would only move from your binding object to the layout. You’ll see a suboptimal way of setting this up first, and then take a look at the built-in two-way binding (@={variable}) syntax.

The examples here are made with the MVVM architecture in mind, but they apply to any object that’s being attached to a view with Data Binding. Also bear in mind that these examples use View Models, but they are not using the Jetpack library of the same name. Here, a View Model is simply a class that holds view state. You can make it extend from the AndroidX ViewModel class if you are so inclined, but that goes beyond the scope of this blog post.

Simple Two-Way Data Binding with Listeners: A Quick and Dirty Approach

A quick way to achieve two-way Data Binding works in the same way as regular, “one-way” Data Binding. You probably wouldn’t want to use this in your app (as you’ll see later on), but instead as an intermediate step towards achieving your first two-way Data Binding setup. Consider that you’re making a password creation screen, and you want to show a password strength indicator as your user creates a password. To start, your layout looks like this:

password_view.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="viewModel"
            type="com.example.PasswordViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

            <EditText
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@{viewModel.passwordQuality}"/>

    </LinearLayout>

</layout>

And your View Model looks like this:

PasswordViewModel.kt

class PasswordViewModel : BaseObservable() {

    var password = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.passwordQuality)
        }

    @get:Bindable
    val passwordQuality: String
        get() = when {
            password.isEmpty() -> "Enter a password"
            password == "password" -> "Very bad"
            password.length() < 6 -> "Short"
            else -> "Okay"
        }
}

For the View Model to do its job and calculate the passwordQuality, something needs to be setting the password field every time the user changes the text. One way to accomplish this is to bind a TextWatcher to the EditText using Data Binding — just like you can bind a String to an EditText. To start, you’ll add a new property to your View Model:

PasswordViewModel.kt

class PasswordViewModel : BaseObservable() {
    ...

    @get:Bindable
    val passwordTextWatcher = object : TextWatcher() {
        override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
            // Do nothing.
        }

        override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
            password = s.toString()
        }

        override fun afterTextChanged(s: Editable) {
            // Do nothing.
        }
    }
}

Then you can add the app:textChangedListener attribute in your layout’s XML file:

password_view.xml

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:textChangedListener="@{viewModel.passwordTextWatcher}" />

When you’re ready to try it out, you can go ahead and click the run button only to be greeted by a fairly long compiler warning. That’s because the Data Binding compiler plugin doesn’t know how to set the textChangedListener property of an EditText since there isn’t a method on EditText that’s called setTextChangedListener.

To fix this, you’ll have to create a BindingAdapter to tell the compiler how to set a TextWatcher on an EditText. To do this, you can create a new file with the following function, or add it to your existing binding adapters:

EditTextBindingAdapters.kt

@BindingAdapter("textChangedListener")
fun bindTextWatcher(editText: EditText, textWatcher: TextWatcher) {
    editText.addTextChangedWatcher(textWatcher)
}

Now when you launch this screen, you’ll see the password strength indicator updating to match what you type. This is very similar to how you would normally accomplish this using the MVC pattern – you’re creating a listener that updates the layout and attaching that listener to the EditText. The only difference here is that Data Binding is taking care of attaching the TextWatcher to the EditText instead of you doing it manually.

One difference between the MVC approach and the MVVM approach in this example is that you have to be very careful about not binding a second TextWatcher. If your View Model accidentally calls notifyPropertyChanged(BR.passwordTextWatcher) or notifyChange(), then you’ll add another TextWatcher. This will waste memory, and if your View Model does this too often, you’ll notice your layout’s performance decrease dramatically when the user types into the field because of the unnecessary work it’s doing.

Implementing two-way Data Binding in this way is very tedious and error-prone. It only pushes the problem of setting up this TextWatcher from your Activity or Fragment into your Data Binding variables. On top of that, it introduces another layer of complexity by forcing you to be mindful of all your listeners! Luckily, Data Binding has a built-in solution for this problem.

Two-Way Data Binding with Binding Adapters

Instead of having to create an XML attribute to bind a listener, you can take advantage of Data Binding’s built-in support for two-way bindings. Behind the scenes, the generated Binding class for your layout is still going to create a listener, but it will do all the heavy-lifting to maintain and keep track of this listener.

Go back to the original layout and View Model you started out with. Instead of adding a passwordTextWatcher property to your View Model, you’ll make sure that your password property is a mutable var as shown below. The ultimate goal here is to get Data Binding to call your setter. Make sure you also update your password property’s setter with a notifyPropertyChanged call as shown below:

PasswordViewModel.kt

class PasswordViewModel : BaseObservable() {

    @get:Bindable
    var password = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.passwordQuality)
            notifyPropertyChanged(BR.password)
        }

    ...
}

Next, update your EditText like this:

password_view.xml

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={viewModel.password}" />

Note the = in the Data Binding operator. This will cause the EditText to be populated with the value of password from the View Model as you’re used to, but the addition of the = will cause the password’s setter to be called whenever the text changes. This means that your PasswordViewModel’s password property will always contain the text in the EditText.

The next time you run your app, you’ll see that the password strength label still updates without you having to write a single BindingAdapter and without creating a listener yourself. Behind the scenes, your binding class is creating its own TextWatcher that functions similarly to the one that you defined manually and is binding and managing the TextWatcher itself.

Writing Your Own Inverse Binding Adapter

Just like how you have to write your own binding adapters to tell Data Binding how to call setters on views it doesn’t know about, you’ll sometimes have to the same thing for the views getters when you use two-way Data Binding.

Suppose that after a user has created a password, you want to show them legal information before they can start using the app. Since legal information tends to be very long, you decide to add a button that takes the user back up to the top of the page. When the user is already at the top of the screen, you also want to hide the button.

All of this can be accomplished with two-way Data Binding. You can start out by creating a basic layout as shown:

scrolling_legal_layout.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="viewModel"
            type="com.example.LegalViewModel" />
    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

            <ScrollView
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:text="@string/lorem_ipsum" />

            </ScrollView>

            <android.support.design.widget.FloatingActionButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/ic_scroll_to_top"
                android:layout_gravity="bottom|end" />

    </FrameLayout>

</layout>

Then you can create an empty View Model class:

LegalViewModel.kt

class LegalViewModel : BaseObservable() {

}

The first thing you’ll need to do is to add a scrollY property to your View Model. You can do this just like you would for a regular object:

LegalViewModel.kt

class LegalViewModel : BaseObservable() {

    @get:Bindable
    var scrollY = 0
        set(value) {
            field = value
            notifyPropertyChanged(BR.scrollY)
        }
}

While you’re in the View Model, you can also define the rest of the behavior for the scroll to top button. You can add the following property to determine the floating action button’s visibility:

class LegalViewModel : BaseObservable() {
    ...

    @Bindable
    val scrollToTopFabVisibility: Int
        get() = if (scrollY == 0) {
            View.GONE
        } else {
            View.VISIBLE
        }
    }
}

And another function to act as the click listener for the FloatingActionButton:

class LegalViewModel : BaseObservable() {
    ...

    fun scrollToTop() {
        scrollY = 0
    }
}

When you add the getScrollToTopFabVisibility() function, remember to update your scrollY property’s setter to dispatch the appropriate notifyPropertyChanged() call as shown below:

class LegalViewModel : BaseObservable() {

    @get:Bindable
    var scrollY = 0
        set(value) {
            field = value
            notifyPropertyChanged(BR.scrollY)
            notifyPropertyChanged(BR.scrollToTopFabVisibility)
        }

    ...
}

Next, it’s time to update the layout XML to tell the Data Binding compiler how to use your newly created functions. You can start out by updating the floating action button as shown:

scrolling_legal_layout.xml

android.support.design.widget.FloatingActionButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/ic_scroll_to_top"
    android:layout_gravity="bottom|end"
    android:visibility="@{viewModel.scrollToTopFabVisibility}"
    android:onClick="@{() -> scrollToTop()}"/>

Then, it’s time to set up two-way Data Binding on the scroll position of your ScrollView. The syntax for this is the exact same as before, using the @={variable} syntax as shown below:

<ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:scrollY="@={viewModel.scrollY}">

If you try to compile the app right now, you’ll get a compiler error since the Data Binding compiler plugin doesn’t know how to get or observe the vertical scroll position – which is to say that the compiler doesn’t know how to read the scrollY value nor how to listen for updates to this value. To fix this, you’ll need to make an Inverse Binding Adapter. This inverse binding adapter will tell Data Binding how to get the current attribute of the view.

Start out by creating a new file for your inverse binding adapter (if you already have a binding adapters file you want to reuse, you can do all of these steps in that file instead if you prefer). Then you can go ahead and create the inverse binding adapter as shown:

ScrollViewBindingAdapters.kt

@InverseBindingAdapter(attribute = "scrollY")
fun getScrollY(scrollView: ScrollView): Int {
    return scrollView.scrollY
}

This gives Data Binding enough information to read the value from the view, but it still doesn’t know how to observe the value on the view. To fix this, you’ll need another binding adapter with a method signature that looks like the one shown below. Note that this example is using OnScrollChangeListener, which was added in Android Marshmallow (API 23).

ScrollViewBindingAdapters.kt

@BindingAdapter(
    value = ["scrollListener", "scrollYAttrChanged"],
    requireAll = false
)
fun setScrollListeners(
    scrollView: ScrollView,
    scrollListener: ScrollView.OnScrollChangeListener?,
    inverseBindingListener: InverseBindingListener?
)

There’s a lot going on here, so it’s a good idea to take a closer look before trying to implement this function, starting with the annotation. Those two strings labeled as value are the names of XML attributes that this Binding Adapter is responsible for. Having "scrollListener" lets you manually set an OnScrollChangeListener in your XML with the app:scrollListener attribute, should the need ever come up in the future.

The "scrollYAttrChanged" label is generated by the Data Binding compiler plugin. Every time you use two-way Data Binding, an internal attribute is created with the AttrChanged suffix. This is the attribute that Data Binding is going to look for when it goes to setup its event listener to update your View Model. Behind the scenes, Data Binding is going to generate code that uses this attribute to bind its observer.

The last thing in the annotation is the requireAll flag. This is simply telling the compiler that this function can be called without all of the parameters. For example, if your layout sets up two-way Data Binding on the vertical scroll position but doesn’t setup a scroll listener (or vice versa), then Data Binding will still call this function but it will pass in null for the scrollListener.

The first two parameters on this function should look pretty familiar. The ScrollView is the view whose scroll position will be observed, and the scrollListener is the optional listener that you can manually attach. The final parameter is a listener that Data Binding expects a callback on when a value changes. In a nutshell, this function’s responsibility will be to create a new OnScrollChangeListener that both wraps the scrollListener and calls the inverse binding listener. This implementation can be achieved like this:

@BindingAdapter(
    value = ["scrollListener", "scrollYAttrChanged"],
    requireAll = false
)
fun setScrollListeners(
    scrollView: ScrollView,
    scrollListener: ScrollView.OnScrollChangeListener?,
    inverseBindingListener: InverseBindingListener?
) {
    val newListener = if (inverseBindingListener == null) {
        scrollListener
    } else {
        object : ScrollView.OnScrollChangeListener() {
            override fun onScrollChange(v: View, scrollX: Int, scrollY: Int, oldX: Int, oldY: Int) {
                scrollListener?.onScrollChange(v, scrollX, scrollY, oldX, oldY)
                inverseBindingListener.onChange()
            }
        }
    }

    scrollView.setOnScrollChangeListener(newListener)
}

Every time that inverseBindingListener.onChange() is called, it signals to the the Binding class for your layout that the scroll position has changed. The binding class will then use the inverse binding adapter from before to get the current vertical scroll position of the view. This value will then be assigned to the scrollY property in your View Model.

If you run the app now and open this layout, you’ll see that everything is behaving exactly as intended – the FloatingActionButton is hidden by default and will appear once the user starts to scroll. This is great, but there’s one really important optimization you should make. Depending on which view you’re using and which attribute your View Model is subscribing to, setting up two-way Data Binding like this can cause an infinite loop. What’s worse is that this infinite loop won’t cause your app to crash or even freeze – it will silently consume CPU resources and drain the battery without any obvious problems.

The example with ScrollView doesn’t exhibit this behavior, but suppose that ScrollView implemented a setScrollY method like this:

public class ScrollView extends View {

    public void setScrollY(int y) {
        mScrollY = y;
        if (mScrollListener != null) {
            mScrollListener.onScrollChanged(this, mScrollX, mScrollY, mScrollX, mOldScrollY);
        }
    }

}


Every time that this ScrollView.setScrollY method is called, your callback is also triggered. Your callback is assigning the scrollY property in your LegalViewModel class from before. For reference, the scrollY setter is implemented like this:

class LegalViewModel : BaseObservable() {

    // Other properties omitted

    @get:Bindable
    var scrollY = 0
        set(value) {
            field = value
            notifyPropertyChanged(BR.scrollY)
            notifyPropertyChanged(BR.scrollToTopFabVisibility)
        }
    }
}

Because of the call to notifyPropertyChanged(BR.scrollY), your layout’s binding class is going to call ScrollView.setScrollY again, which causes this entire cycle to start all over. Because of the way Data Binding implements binding classes, though, it will wait until the next frame before calling setScrollY on the ScrollView.

The solution to this is very simple. In your LegalViewModel, update the scrollY setter to do nothing if the value didn’t change:

class LegalViewModel : BaseObservable() {

    // Other properties omitted

    @get:Bindable
    var scrollY = 0
        set(value) {
            if (field != value) {
                field = value
                notifyPropertyChanged(BR.scrollY)
                notifyPropertyChanged(BR.scrollToTopFabVisibility)
            }
        }
    }
}

It’s a good idea to do this on all the setters that you use with two-way Data Binding. View implementations can change over time so updating one of your libraries or using a different version of Android may cause this infinite loop behavior without any warnings. Adding this check is a simple way to guarantee that your two-way Data Binding won’t cause an infinite loop.

Conclusion

If you’re using MVVM or another similar architecture that takes advantage of Data Binding, then two-way Data Binding is a great way of getting information about your view into your View Models. If you’ve manually been binding listeners, then you should consider to the built-in technique for two-way Data Binding provided by the @={variable} syntax. It allows you to remove all references of view listeners from your View Models, which in turn makes your View Models more simple and easier to test.

If you’re not using MVVM, then two-way Data Binding may not be right for your app. Data Binding on its own works best when you have a model object that can directly be used by the view. If the layout XML has any logic, then suddenly you may find yourself debugging your layouts in addition to Java and Kotlin code. Adding two-way Data Binding without the right architecture can make debugging much more likely and difficult.

When you’re ready to set up two-way Data Binding on one of your layout attributes, there are just a couple of steps. To summarize:

  1. Setup one-way Data Binding for the attribute
  2. Add a setter to your model – make sure that this setter ignores the value if it hasn’t changed
  3. Add an = in your layout to use the @={variable} syntax
  4. Create an inverse binding adapter for the attribute if necessary

Andrew Bailey

Author Big Nerd Ranch

Andrew Bailey is an Android engineer and instructor at Big Nerd Ranch. He graduated from the Georgia Institute of Technology with a degree in computer science. When he’s not building apps or teaching a class, he can be found baking, playing video games, or ruining his dungeon master’s game plans.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News