Search

Descent Into Databinding

Bill Phillips

25 min read

Apr 17, 2016

Descent Into Databinding

Last year, the Android team introduced a new building block for Android development: the Data Binding framework.
Data Binding is supposed to eliminate layout-related boilerplate.

If you’re a practicing Android developer, you might not have leaped on this.
Odds are good that you’re already using a view injection library like ButterKnife, which mitigates the pain Data Binding is meant to address.
And Data Binding does so much more than ButterKnife — maybe it’s not worth the additional complexity and mystery of a giant new tool.

In this post, I’d like to dive into Data Binding with fresh eyes.
I’ll explain how Data Binding works at development time and at build time,
which should clarify some of the rough edges in the current pre-release version.
By the end, I hope to get you to where you can make an informed decision as to whether you want to use Data Binding, as well as how much of it you want to make use of.

Some Code To Play With

For the examples in this post, I will use an example app that is familiar to me: CriminalIntent, from our book Android Programming: The Big Nerd Ranch Guide.

If you’re familiar with the book, I am basing my work off of the solution to Chapter 13 in the 2nd edition.
I’ve modified it slightly to illustrate a few ideas here, and to provide some sample data to play with.
If you want to follow along, clone data-binding-talk and checkout the branch jingibus/starting-point.

CriminalIntent is a master-detail app: the main screen (implemented in CrimeListFragment.java shows a list of workplace crimes, each with a few different properties.
If you tap on a crime, the app pulls up a detail screen to edit the crime in question.
We’ll be playing around with Data Binding in the main list screen:

Main CriminalIntent screen

Turning on Data Binding

Make sure you have the latest version of Android Studio 2.

You’ll also need to update your Gradle version.
Newer versions of Android Studio automatically ask you to update to the latest version, so just click “Update” when it asks you.

If for some reason your Gradle version is not automatically updated, you can manually update it to the latest (2.1.0-alpha4 as of this writing) in your top level build.gradle:

dependencies {
    classpath 'com.android.tools.build:gradle:2.1.0-alpha4'

    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
}

Note that this may cause an error saying that you have the wrong Gradle wrapper.
This will go away if you take the error’s suggestion to reimport the project.

Then turn on data binding in your project by adding the following lines to your app/build.gradle:

android {
    compileSdkVersion 21
    buildToolsVersion "20.0.0"

    ...
    dataBinding {
        enabled = true
    }
}

(Feel free to update the build tools and compileSdkVersion if you like — this is a legacy project, so it has older values out of the box.)

This turns on the build integration for the code generation, but it also turns on Data Binding’s IDE integrations.

Switching From a Regular Layout

To use Data Binding with a specific layout, you have to make a couple of modifications to your code.
The first one will be to your layout.
list_item_crime.xml is a big-ish RelativeLayout-based layout file that looks like this (abbreviated for space):

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

    <CheckBox
        android:id="@+id/solved_check_box"
        .../>

    <TextView
        android:id="@+id/title_text_view"
        .../>

    <TextView
        android:id="@+id/date_text_view"
        .../>

</RelativeLayout>

At its heart, Data Binding does one big thing:
it takes a layout file (like list_item_crime.xml) and generates a corresponding Java class, called a binding class.

It will not do this until you tell Data Binding to do its thing, though.
To do that, you wrap your existing layout file in a new tag: <layout>.

<layout>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                    xmlns:tools="http://schemas.android.com/tools"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent">

        ...

    </RelativeLayout>
</layout>

When you build and deploy to your device, Data Binding will generate an associated class with a similar name, just in CamelCase: ListItemCrimeBinding.

Unlike generated code tools like Dagger, Data Binding does not rely on generated code for type checking.
Instead, it is integrated into Android Studio, so that you do not have to wait through a whole code generation pass to use the fields and methods Data Binding provides.

As of this writing, this integration needs a little jump-start to get going.
To make ListItemCrimeBinding available after adding the <layout> tag, you must restart Android Studio, then rebuild the project.

Once that is done, you can integrate the binding class into your project.
Here, that will be in your ViewHolder implementation, CrimeListFragment.CrimeHolder:

private class CrimeHolder extends RecyclerView.ViewHolder
        implements View.OnClickListener {

    private final ListItemCrimeBinding mBinding;
    private TextView mTitleTextView;
    private TextView mDateTextView;
    private CheckBox mSolvedCheckBox;

    private Crime mCrime;

    public CrimeHolder(ListItemCrimeBinding binding) {
        super(binding.getRoot());
        mBinding = binding;
        itemView.setOnClickListener(this);

        ...
    }

Instead of taking in a View, CrimeHolder now takes in ListItemCrimeBinding, which has the View as its root.

To create the instance of ListItemCrimeBinding itself, you use DataBindingUtil instead of directly calling LayoutInflater.inflate.
In this case, inflation happens inside CrimeListFragment.CrimeAdapter.onCreateViewHolder.
The Data Binding version of that code looks like this:

private class CrimeAdapter extends RecyclerView.Adapter<CrimeHolder> {

    private List<Crime> mCrimes;

    public CrimeAdapter(List<Crime> crimes) {
        mCrimes = crimes;
    }

    @Override
    public CrimeHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
        ListItemCrimeBinding binding = DataBindingUtil
                .inflate(layoutInflater, R.layout.list_item_crime, parent, false);
        return new CrimeHolder(binding);
    }

With that, you’re using Data Binding.

Great.
Now what?

View Binding

The moment you add a <layout> tag and generate a binding class, Data Binding is doing useful work.
All generated binding classes have a camelCase generated field for each android:id that is assigned in the layout file.
When the layout is inflated, those views are extracted and bound to their associated fields.

That means that the moment you have an instance of ListItemBinding, you can immediately ditch all of your findViewById related code.
All of the fields and findViewById calls currently in CrimeHolder can be removed:

private class CrimeHolder extends RecyclerView.ViewHolder
        implements View.OnClickListener {

    private final ListItemCrimeBinding mBinding;

    private Crime mCrime;

    public CrimeHolder(ListItemCrimeBinding binding) {
        super(binding.getRoot());
        mBinding = binding;
        itemView.setOnClickListener(this);
    }

    public void bindCrime(Crime crime) {
        mCrime = crime;
        mBinding.titleTextView.setText(mCrime.getTitle());
        mBinding.dateTextView.setText(mCrime.getDate().toString());
        mBinding.solvedCheckBox.setChecked(mCrime.isSolved());
    }

Pretty handy.

IDE Integration

If you are like me, and you know that this framework generates code, you will want to try and look at the code by doing something like this:

Navigate to bound view

If you do, you may be surprised (like I was).
Instead of taking you to the generated code, it takes you to the layout file.

The reason this happens is the same reason navigating to the declaration of R.layout.list_item_crime takes you to the layout file instead of the generated R.java file:
the IDE integration does error checking without the generated code, so that you do not have to rebuild to use a generated field.
If you already know Android Studio inside and out, this is very confusing.
In the long run the IDE integration should have the same benefits that the resource ID integration has.

Note that none of this applies if you’re building outside of Android Studio, where it will behave identically to a tool like Dagger.
It’s also possible to use apt to skirt around the IDE integration and force the behavior of a typical generated code library.
I don’t recommend it, though — it may feel more familiar initially, but it will be slower in the long run.

Binding Expressions

Data Binding also gives you the ability to use binding expressions.
Most of this article will be concerned with binding expressions in some way.

The basic idea of a binding expression is small: a binding expression is an expression in an XML attribute that tells your binding class to set a value on a view object.
Binding expressions are very close to Java expressions, plus some additional syntactic sugar, like resource references, to simplify writing view logic.

Binding expressions are particularly handy when you want to assign a value that relies on an existing value in some way, but doesn’t really deserve its own name.
For example, in this version of CriminalIntent, we have the following value assigned to solved_check_box:

android:padding="@dimen/list_item_padding_2x"/>

As you might imagine, list_item_padding_2x is twice the value of list_item_padding.
You can use a binding expression to instead write it this way:

android:padding="@{@dimen/list_item_padding * 2}"/>

Note the special @{} binding mustache syntax.
This signals that the attribute will be processed by Android Data Binding.
Data Binding will read the expression as it generates the binding class, and generate code to assign this value at runtime.
The XML attribute itself is stripped out of the XML that ships with your app.

Simple Data Binding: No Data Section

Before I continue, I want to mark this down as a potential stopping point for you in your own work.
Admittedly, it’s a bit like heading out on the Oregon Trail from Missouri and stopping in Nebraska.
But there’s a lot to be said for Nebraska, and it’s a long haul to the other side of the Rockies.

So here we are: you can use Data Binding for view binding and basic binding expressions, and stop there.
Any further usage of Data Binding requires a <data> section (described below).
If you disallow the use of this section in code review, you limit your usage of the tool to these two mechanisms.
If you’re interested in seeing a project at this level of integration, checkout the jingibus/no-data-section branch of data-binding-talk.

The biggest reason I like this stopping point is view binding.
View binding dramatically reduces the number of times you have to refer to view names in your code.

Take the name title text view from above as an example.
Without data binding, you have at least 5 places title text view must appear in some form:

  • @+id/title_text_view in the layout file
  • R.id.title_text_view in the call to findViewById
  • mTitleTextView in the field definition
  • mTitleTextView in the assignment when findViewById is called
  • Finally, mTitleTextView when you actually use the widget

ButterKnife cuts that down to four:

  • @+id/title_text_view in the layout file
  • R.id.title_text_view in your @Bind annotation
  • mTitleTextView in the field definition
  • mTitleTextView when you use the widget

If you use Data Binding instead, that’s further cut in half:

  • @+id/title_text_view in the layout file
  • titleTextView when you use the widget

I think that’s a pretty clear win.
The only other tool I know of that competes with Data Binding’s view binding is Kotlin’s Android view extensions,
and that requires you to migrate to Kotlin.
I love Kotlin, but that’s a much bigger change than Data Binding is.

Data Variables

While the layout tag enrolls you into the cult of Data Binding, delving into its mysteries requires a new element in your XML file: <data>.
This section goes right after your initial layout tag:

<layout>
    <data>

    </data>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        ...

In the data section, you can define variables on the layout.
This gives you the ability to send objects into the layout file.
So you could define a variable for the crime’s title:

<layout>
    <data>
        <variable
            name="crime"
            type="com.bignerdranch.android.criminalintent.Crime"/>
    </data>

Defining a variable in the data section creates a property on the associated binding class.
So if you jump back over to CrimeListFragment, you will see that ListItemCrimeBinding now has a setCrime setter:

public void bindCrime(Crime crime) {
    mCrime = crime;

    mBinding.setCrime(mCrime);
    mBinding.titleTextView.setText(mCrime.getTitle());
    mBinding.dateTextView.setText(mCrime.getDate().toString());
    mBinding.solvedCheckBox.setChecked(mCrime.isSolved());
}

Once you have defined a variable, you can use it within binding expressions.
For example, you could wire up dateTextView, including a formatting message:

<TextView
    android:id="@+id/date_text_view"
    android:text="@{`Date discovered: ` + crime.getDate().toString()}"
    .../>

(Inside of binding expressions, backticks are interpreted as double quotes. Handy.)

Almost everything that you can write in a Java expression is valid in a data binding expression:
boolean logic, comparisons, ternary logic operators, and so forth.
You cannot use this, super, or new, and you cannot explicitly invoke generics, but everything else is available to you.

Formatting Strings

Of course, you would not want to use a string literal in your Java code.
You would want to use a formatting string instead.

<resources>
    ...
    <string name="hide_subtitle">Hide Subtitle</string>
    <string name="subtitle_format">%1$s crimes</string>
    <string name="list_date_format">Date discovered: %1$s</string>
</resources>

You can use formatting strings in binding expressions as functions, passing in the formatting parameters.

<TextView
    android:id="@+id/date_text_view"
    android:text="@{@string/list_date_format(viewModel.getDate().toString())}"
    .../>

Listeners & Lambdas

Android Studio 2.0 beta 6 also introduced a lambda syntax for hooking up event listeners and other callbacks in binding code.
CrimeHolder is responsible for handling view clicks.
If you add it as a variable:

<layout>
    <data>
        <variable
            name="crime"
            type="com.bignerdranch.android.criminalintent.Crime" />
        <variable
            name="holder"
            type="com.bignerdranch.android.criminalintent.CrimeListFragment.CrimeHolder"/>
    </data>
    ...

And assign it inside of CrimeListFragment.CrimeHolder:

public CrimeHolder(ListItemCrimeBinding binding) {
    super(binding.getRoot());
    mBinding = binding;
    mBinding.setHolder(this);
}

You can wire up the call to holder.onClick directly in the layout file.

<layout>
    <data>
        <variable
            name="crime"
            type="com.bignerdranch.android.criminalintent.Crime" />
        <variable
            name="holder"
            type="com.bignerdranch.android.criminalintent.CrimeListFragment.CrimeHolder"/>
    </data>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                    xmlns:tools="http://schemas.android.com/tools"
                    android:onClick="@{(view) -> holder.onClick(view)}"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent">

The expression (view) -> holder.onClick(view) is a lambda expression.
Data Binding lambda expressions are a limited, abbreviated version of Java 8 lambdas:
no types are permitted in the variable names, and no code blocks are permitted in the body of the lambda.
You can’t leave off the parens, but you can omit the variable name if you aren’t using it.

At runtime, the lambda will behave as if you had defined a complete listener implementation:

new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        holder.onClick(view);
    }
};

Mechanics of View Binding

So how does it work?
When are binding expressions run?
How they triggered?
I could talk about it, but I’d rather show you.

As mentioned before, Data Binding is integrated into the IDE, so it does not always generate code.
You can force code to be generated, though.
Run your project, and if your build and deploy is successful, you should have generated code.
Switch to the Project view in your Project tab in Android Studio.

The generated code goes in app/build/intermediates/classes/debug (or classes/release, for a release build).
Deep inside classes/debug, in com/bignerdranch/android/criminalintent/databinding, you will find the generated ListItemCrimeBinding.java class.

Navigating to generated code

Double click to crack it open.

We know that the views must be repopulated when you call setCrime.
Here’s what setCrime looks like as of this writing (no guarantees it will stay the same):

public void setCrime(com.bignerdranch.android.criminalintent.Crime crime) {
    this.mCrime = crime;
    synchronized(this) {
        mDirtyFlags |= 0x2L;
    }
    super.requestRebind();
}

Each individual source value that can be displayed has its own dirty bit in mDirtyFlags.
When that source value changes, the dirty bit is flipped and a rebind is requested.

What does it mean to request a rebind? Well, here’s what requestRebind() looks like:

protected void requestRebind() {
    synchronized (this) {
        if (mPendingRebind) {
            return;
        }
        mPendingRebind = true;
    }
    if (USE_CHOREOGRAPHER) {
        mChoreographer.postFrameCallback(mFrameCallback);
    } else {
        mUIThreadHandler.post(mRebindRunnable);
    }

}

Requesting a rebind simply posts a callback to be run on the main thread as soon as possible.
So you can assign new values to three different variables, and it will only run the bindings one time.
(The actual method on ListItemCrimeBinding that assigns the values is called executeBindings(). We’ll check that out in a minute.)

This is a problem in a RecyclerView, though.
A RecyclerView could be scrolling very quickly.
If rebinding does not happen immediately, there is a possibility of visible flicker.

There is a straightforward fix, thankfully.
If you need the bindings executed immediately, just call executePendingBindings() on your binding class.

public void bindCrime(Crime crime) {
    mCrime = crime;

    mBinding.setCrime(crime);
    mBinding.executePendingBindings();
}

This will run the code for all of your binding expressions, and unset the dirty bits for all of the variables you changed.

Syntactic Goodies

As was mentioned above, binding expressions are not only Java expressions.
They’re intended to write small bits of code to wire up views.
To make that easier, binding expressions have some additional syntax and semantics differences that make it easier to write brief view logic.

Null Receivers

The first semantic difference is that binding expressions generate code that treats null differently the regular Java code.
If you dive into ListItemCrimeBindings.executeBindings(), you will find a lot of code like this:

if (crime != null) {
    // read crime~~getDate~crime~
    crimeGetDateCrime = crime.getDate();
}

if (crimeGetDateCrime != null) {
    // read crime~~getDate~crime~~toString~crime~~getDate~crime~
    crimeGetDateCrimeToS = crimeGetDateCrime.toString();
}

No methods are ever called without first verifying that the method’s recipient is not null.
The effect of this is that null valued objects behave as if they receive messages, like in Obj-C.
Method invocations on null run no code, and always yield default values.

This preserves some existing Java behavior, while making it more resilient to the presence of null.
Take the example from earlier, where you used string concatenation:

"@{`Date discovered: ` + crime.getDate().toString()}"

If crime or crime.getDate() is null here, the entire crime.getDate().toString() expression ends up being null.
Since Java string concatenation converts null values to the string "null",
the whole expression evaluates to "Date discovered: null.

This behavior prevents many NullPointerExceptions, but not all.
null receivers may send null values on to other methods.
For example, if you wrote the following:

"Date discovered: ".concat(crime.getDate().toString())

concat throws a NullPointerException if it receives a null parameter.
So you’d get an NPE if crime were null.

Null Coalescing

Another way binding expressions help abbreviate null handling is through the null coalescing operator, ??.
The null coalescing operator helps when you want to provide a default value when some other value is null.
Take this for example:

"@{@string/list_date_format(crime.getDate().toString() ?? `(no date)`)}"

Instead of showing "Date discovered: null" for a null value, you would see "Date discovered: (no date)".

Properties

You also don’t have to write out property getters.
So instead of:

"@{@string/list_date_format(crime.getDate().toString() ?? `(no date)`)}"

You can write:

"@{@string/list_date_format(crime.date.toString() ?? `(no date)`)}"

This works for other standard getter conventions like isSolved(), too.

Gotchas

There are a couple of gotchas to keep in mind with Data Binding right now.
How temporary these are is unknown, but for now they are good to know about.
(They shed some light on things worth knowing, too.)

Layout Attributes

Remember that earlier I said that setting a binding expression on an attribute causes that attribute to be processed by Data Binding, not by the XML layout inflation process.
And as we have seen, binding expressions are all about setting values on view objects.

What happens when you set a binding expression on a layout parameter, though?
You might want to do the same trick with margins that you used on padding:

...
<TextView
    android:id="@+id/date_text_view"
    android:text="@{@string/list_date_format(crime.date.toString() ?? `(no date)`)}"
    android:layout_margin="@{@dimen/list_item_padding * 2}"
    .../>
...

This will throw an error:

~/src/android/CriminalIntent/app/src/main/res/layout/list_item_crime.xml
Error:(38, 38) Cannot find the setter for attribute 'android:layout_margin' 
with parameter type float.

This is because, as of right now, data binding is a mechanism for setting values on view objects.
That means that under the hood, all of your attribute setting is being converted to method calls.
Which is what the error is complaining about: it cannot find a setter for a property named layout_margin.
More about that in a moment.

Generated Code Errors

In current versions of Data Binding, XML errors can be confusing.
For example, if you wrote the following in your XML file:

...
<TextView
    android:id="@+id/date_text_view"
    android:text="@{@string/list_date_format(crime.date.toString() ?? `(no date)`)}"
    android:allCaps="true"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_toLeftOf="@id/solved_check_box"
    android:layout_below="@id/title_text_view"
    tools:text="Crime Date"
    android:padding="@{@dimen/list_item_padding * 2}"/>
...

The attribute android:allCaps does not exist — it’s actually android:textAllCaps.
If you try to run the app, you get the following error:

~/src/android/CriminalIntent/app/build/intermediates/data-binding-layout-out/debug/layout/list_item_crime.xml
Error:(35) No resource identifier found for attribute 'allCaps' in package 'android'

So you say, “D’oh,” as is the fashion at this time, and double click on the error to go fix it.
That will pop you into this code:

...
<TextView
    android:id="@+id/date_text_view"
    android:tag="binding_3"
    android:allCaps="true"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_toLeftOf="@id/solved_check_box"
    android:layout_below="@id/title_text_view"
    tools:text="Crime Date"
...

At first glance, this looks like the same place you just were.
You might even fix the offending line here and rebuild.

It won’t work though, because this is actually a post-processing version of the file.
Remember above when I said that XML attributes are stripped out to be processed by the data binding tool?
If you look closely, you will see that this listing is actually missing two attributes:
android:text and android:padding.
This is your layout XML after those attributes are stripped.
Those attributes were assigned with binding expressions, and so the code for them is in ListItemCrimeBinding.

If you run into this problem, it means that you will have to manually reopen the layout file in res/layout.
If you do not, you will get into a loop where you think you have fixed the problem, and then find the error you fixed pops up again immediately.
I have found myself going through a couple of cycles of this, even though I know about it.
But hey — at least I know.

Attribute Processing

Strangely enough, you actually can get that non-existent android:allCaps attribute to work.
All you have to do is change its assignment to use a binding expression.

...
<TextView
    android:id="@+id/date_text_view"
    android:text="@{`Date discovered: ` + crime.date.toString() ?? `(no date)`}"
    android:allCaps="@{true}"
    .../>
...

Remember that a binding expression says, “Data Binding, please process this attribute assignment for me.”
The default way Data Binding processes an attribute is to look for a single-parameter setter on the view with the same type as the binding expression.
And, as it turns out, the setter associated with the android:textAllCaps attribute is called setAllCaps(boolean).
So android:allCaps works perfectly fine.

This is what’s called an automatic setter.
If a setter exists on a view, you can always set it by using a binding expression with the appropriate type.

Non-standard Attributes

Some widget properties you would want to assign aren’t single-parameter setters, though.
And some attributes, like android:textAllCaps, don’t have the same name as their setters.

For example, say you had an EditText that you wanted to setup a text listener for.
There is no documented way to configure this interface in XML.
In Java, you would call addTextChangedListener with an implementation of the TextWatcher interface:

public interface TextWatcher extends NoCopySpan {
    void beforeTextChanged(CharSequence var1, int var2, int var3, int var4);
    void onTextChanged(CharSequence var1, int var2, int var3, int var4);
    void afterTextChanged(Editable var1);
}

Can you use it with Data Binding?
If so, how do you find out how it works?

Fantastic BindingAdapters And Where To Find Them

There is not yet any official documentation, but there is still a way to find these attributes.
All custom attribute behavior is implemented through binding adapters.
If you need to know what custom attributes apply to a kind of widget, all you need to do is find its binding adapters and you will see all the custom attributes.

The binding adapters are written in a class with the widget’s class name, plus Adapter on the end.
So for TextView, you can find the binding adapters by going to Navigate->Open class in Android Studio (or Cmd-O, if you’re using the same keybindings as me).
Type in TextViewBindingAdapter, and open up the class that it finds.

At the top, you’ll initially see this:

@BindingMethods({
        @BindingMethod(type = TextView.class,
                       attribute = "android:autoLink",
                       method = "setAutoLinkMask"),
        ...
        @BindingMethod(type = TextView.class,
                       attribute = "android:onEditorAction",
                       method = "setOnEditorActionListener"),
})
public class TextViewBindingAdapter {
    ...

For attributes like android:textAllCaps, where the method name does not match up with the attribute name, a BindingMethod attribute points Data Binding in the right direction.

This isn’t the case for TextWatcher, so you will not find what you need in this section.
Type Cmd-F to do a search in this file, and look for TextWatcher.
You should jump right to this method:

...
@BindingAdapter(value = {"android:beforeTextChanged",
                         "android:onTextChanged",
                         "android:afterTextChanged"},
                requireAll = false)
public static void setTextWatcher(TextView view, final BeforeTextChanged before,
        final OnTextChanged on, final AfterTextChanged after) {
    ...

Paydirt.
This is what a binding adapter implementation looks like.
It is an annotated static method, which may handle one or more attributes.

The first parameter of the method is always the kind of view the binding adapter applies to.
Following that is one parameter for each attribute handled by the adapter.
This adapter handles three attributes, one for each method on the TextWatcher interface.
The parameters correspond, in order, to the list of attribute names specified in the @BindingAdapter annotation’s value parameter.

Now you know how to configure a TextWatcher callback.
OnTextChanged has one method, onTextChanged, which takes in four parameters: a String, and three ints.

public interface OnTextChanged {
    void onTextChanged(CharSequence s, int start, int before, int count);
}

So to listen to TextWatcher.onTextChanged(), you would write the following binding expression in your layout file:

android:onTextChanged="@{(s, start, before, count) -> holder.onTitleTextChanged()}"

Lambdas let you omit the callback parameters, too, if you aren’t using them.
So you could also write your callback like so:

android:onTextChanged="@{() -> holder.onTitleTextChanged()}"

Writing Your Own Attributes

If you need an attribute that is not provided, you can write your own binding adapters.
Complete coverage of that topic is a little beyond the scope of this article, but the source for TextViewBindingAdapter above is a great place to start.
The Data Binding Guide also explains this topic in some detail.

One Last Thing: ViewModel

I’ve shown you how to use Data Binding to pull values from model objects into your layout automagically.
Unfortunately, I’ve also made a mess: my layout is integrating code from a few different areas.
That puts a lot of real responsibility in the layout file.

That’s a bad thing.
Responsibility means you need to provide oversight.
Oversight means code review and tests.
You don’t want to code review your layout files in this way — the functionality will disappear amongst all the display concerns.
And you definitely don’t want to put it under test.

So the last bit of cleanup work to do here is to take the interaction and data formatting pieces out of the layout file and into a new class that will take on that responsibility: a view model.

I create my class, and call it CrimeListItemViewModel, since its responsibility is to the crime list item.
I add a couple of dependencies:

public class CrimeListItemViewModel {
    private final Context mContext;
    private Crime mCrime;

    public CrimeListItemViewModel(Context context) {

        mContext = context.getApplicationContext();
    }

    public Crime getCrime() {
        return mCrime;
    }

    public void setCrime(Crime crime) {
        mCrime = crime;
    }
}

This view model is showing information about a Crime, so it definitely needs a Crime.
It will start a new activity when it is selected, too, so it needs a Context.

Then you expose getters and event trigger methods:

    ...
    public CrimeListItemViewModel(Context context) {
        mContext = context;
    }

    public Crime getCrime() {
        return mCrime;
    }

    public void setCrime(Crime crime) {
        mCrime = crime;
    }

    public String getTitle() {
        return mCrime.getTitle();
    }

    public String getRenderedDate() {
        return mCrime.getDate().toString();
    }

    public boolean isSolved() {
        return mCrime.isSolved();
    }

    public void onCrimeSelected() {
        Intent intent = CrimePagerActivity.newIntent(mContext, mCrime.getId());
        mContext.startActivity(intent);
    }
}

Update the layout file to use an instance of CrimeListItemViewModel instead of Crime and CrimeHolder.

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <variable
            name="viewModel"
            type="com.bignerdranch.android.criminalintent.CrimeListItemViewModel" />
    </data>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                    xmlns:tools="http://schemas.android.com/tools"
                    android:onClick="@{() -> viewModel.onCrimeSelected()}"
                    ...>

        <CheckBox
            android:id="@+id/solved_check_box"
            android:checked="@{viewModel.solved}"
            .../>

        <TextView
            android:id="@+id/title_text_view"
            android:text="@{viewModel.title}"
            .../>

        <TextView
            android:id="@+id/date_text_view"
            android:text="@{`Date discovered: ` + viewModel.renderedDate ?? `(no date)`}"
            ...
            android:padding="@{@dimen/list_item_padding * 2}"/>

    </RelativeLayout>
</layout>

Then change your CrimeHolder to use the view model, too.

public class CrimeHolder extends RecyclerView.ViewHolder {
    private final ListItemCrimeBinding mBinding;
    private final CrimeListItemViewModel mViewModel;

    public CrimeHolder(ListItemCrimeBinding binding) {
        super(binding.getRoot());
        mBinding = binding;
        mViewModel = new CrimeListItemViewModel(getActivity());
        mBinding.setViewModel(mViewModel);
    }

    public void bindCrime(Crime crime) {
        mViewModel.setCrime(crime);
        mBinding.executePendingBindings();
    }
}

This idea of using a view model class is often called “model-view-viewmodel”, or MVVM.
This is not a complete coverage of the idea of MVVM architecture.
But it does show the essentials of the idea.

Observable Objects

Look great, right?
Not if you scroll down, it’s not:

borked list of crimes

The list items are never getting updated with the new information being sent to ListItemCrimeViewModel.
And how could they? The binding object doesn’t know.

One fix is to add a call to ListItemCrimeBinding.invalidateAll(), which will trigger a rebind of everything.
Far less brittle, though, is to use an observable object instead.

A observable Data Binding object implements the android.databinding.Observable interface.
This is a different from RxJava’s Observable interface: a Data Binding Observable exposes the ability to listen to changes on an individual properties of an object.

Implementing Observable requires writing plumbing to hook up listeners to each individual property.
Rather than writing this yourself, you are almost always better off extending BaseObservable instead:

public class CrimeListItemViewModel extends BaseObservable {
    ...

You then need to specify which properties of your class may be bound to by annotating them with @Bindable:

    ...
    @Bindable
    public String getTitle() {
        return mCrime.getTitle();
    }

    @Bindable
    public String getRenderedDate() {
        return mCrime.getDate().toString();
    }

    @Bindable
    public boolean isSolved() {
        return mCrime.isSolved();
    }
    ...

The last step is to notify whoever is observing your object when one of these properties changes.

Typically, this is done by calling notifyPropertyChanged(int).
The int is a special constant for each property name in a file called BR.java.

BR.java is a generated file similar to R.java.
Instead of containing resource IDs, BR.java contains binding resource IDs — integer constants that can be used to identify properties by name instead of Strings.
When you mark a field or getter with @Bindable, a matching constant with the same name is added to BR.java.
So to say that getTitle()’s value has changed, you would call:

notifyPropertyChanged(BR.title);

In CrimeListViewModel, the only thing that triggers property changes is a call to setCrime(Crime), and that triggers changes to all the properties.
Which you can signify by just calling notifyChange():

    public void setCrime(Crime crime) {
        mCrime = crime;
        notifyChange();
    }

This signals that all of the object’s properties have changed.

Non-simple Data Binding: MVVM

It was quite a lot of ground to cover, but now you’re at the other end of the trail.
(If you check out the code from the repo, this is what you see on the master branch.)
If you want to use Data Binding to its fullest potential, I recommend going all the way here.
Stopping without going all the way to a View Model architecture is like stopping in east Oregon — Portland is almost there!
And there are food trucks there!

Seriously, though, there are some very nice things about this architecture, particularly if you’re doing testing.

  • View models are easy to test.
    Click handling is tested by invoking a method and seeing what happens.
    Property values are verified by calling input data setters and seeing what the output properties show.
  • Fewer ids, fewer names.
    Views that are filled in with data binding don’t need to be referenced in code, so they don’t need ids.
    If our example code didn’t use RelativeLayout, it would not need any ids at all.
  • You can read the layout file and see what data goes where, rather than inferring it from the name of each component.

Not everyone will find it worthwhile to make the migration to this full-blown usage of data binding.
If you don’t, I recommend stopping at the simple usage described above in the “Simple data binding” section.

In Conclusion: Opinions

Life is easier without them, but I have a hard time writing conclusions without sharing an opinion.
So here’s some short and sweet spit takes:

My first one might be a bit controversial: if you’re using apt with Data Binding, I recommend stopping.
It’s only slowing you down.
Relying on the IDE integration will make it easier for you to navigate your codebase, and eliminate the need to rebuild to make fields visible.

If you can pair MVVM with Binding, I recommend doing so.
We’ve already used it in some production apps, to good effect.
The initial hurdles can be a little painful, but it really kills a lot of annoying boilerplate in your Java code.

If you cannot use a View Model architecture, I still recommend using Data Binding, but only as a view binding tool.
Without MVVM or a similar discipline governing what kinds of objects you use in your layout file,
your layout files can easily become a locus of maintenance nightmares.
Debugging is hard enough without having to add your resources folder to the places you need to hunt down business logic.

So don’t go halfsies on this.
There may be other sweet spots for Data Binding usage.
MVVM and view binding are the two I can recommend today, though.

And I think that’s about all I have to say about Data Binding.

Juan Pablo Claude

Reviewer Big Nerd Ranch

During his tenure at BNR, Juan Pablo has taught bootcamps on macOS development, iOS development, Python, and Django. He has also participated in consulting projects in those areas. Juan Pablo is currently a Director of Technology focusing mainly on managing engineers and his interests include Machine Learning and Data Science.

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