Android data binding

Every Android developer knows the usual tiresome way of binding their application logic with layouts. Tens or even hundreds of boilerplate code lines containing the findViewById methods and never-ending manual updating of view properties based on changes in data. Thankfully, Android data binding support library comes to help automate things and simplify your code.

Android data binding support library

Android data binding support library has been released along with Android Marshmallow. Being a support library, data binding is therefore available in all Android versions back to SDK v7.

To enable data binding in your project, Gradle 1.5.0 and higher is required and the following lines present in the module level build.gradle file of your project:

android {
    ...
    dataBinding {
        enabled = true
    }
}

After a project sync, you can make full use of the data binding in Android.

Let’s see what you can achieve with it in a simple example project showing a list of articles, each containing a featured image, article title, excerpt, a button with the number of comments and a button navigating to a hypothetical detail of the article. The application looks like this on a device:

To make our example application complete, let me first present the content of the holding activity class and its layout file. The activity does nothing but holds a single RecyclerView displaying the content of list of Article data objects using a custom adapter. Data binding is used here as well so don’t worry about not understanding every line of code. They will be explained in short.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" >
    <data/>
    <android.support.v7.widget.RecyclerView
        android:id="@+id/contact_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        android:paddingBottom="20dp" />
</layout>

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
        RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
        binding.contactList.setLayoutManager(layoutManager);

        List<Article> articles = new ArrayList<>();

        /* filling the list with Article data objects */

        ArticleAdapter adapter = new ArticleAdapter(articles);
        binding.contactList.setAdapter(adapter);
    }
}

All we need to do now is to create a layout XML file representing the article item, an Article data model class and an adapter for the RecyclerView. We shall look into these in more detail in the following sections.

Data model objects

Any plain old Java object (POJO) can be used as a data supply for data binding in Android. However, by doing only this, you would lose one of the main advantages data binding usage has and that’s automatic view updates upon object properties change.

In our example, this is how the Article data object could look like:

public class Article {

    private String title;
    private String excerpt;
    private boolean highlight;
    private String imageUrl;
    private int commentsNumber;
    private boolean read;

    public Article(String title, String excerpt, boolean highlight, 
                   String imageUrl, int commentsNumber) {
        this.title = title;
        this.excerpt = excerpt;
        this.highlight = highlight;
        this.imageUrl = imageUrl;
        this.commentsNumber = commentsNumber;
        this.read = false;
    }

    /* getters and setters */
}

In order to make use of automatic view updates, there are three options you could go with. You could either create an observable object, observable fields, or observable collections.

The easiest way to go for a developer is to use a data class implementing the Observable interface. Android provides a convenience class BaseObservable which your data classes can extend and which takes care of listener registration. The developer is, however, still responsible for notifying of property changes. This is done by assigning the @Bindable annotation to the property getter and notifying in the property setter.

This is the Article class updated to extend the BaseObservable class:

public class Article extends BaseObservable {

    /* data properties */
    /* constructor */

    @Bindable
    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
        notifyPropertyChanged(BR.title);
    }

    public boolean isRead() {
        return read;
    }

    public void setRead(boolean read) {
        // change title of already read article:
        if (read && !this.read) {
            setTitle("READ: " + getTitle());
        }

        this.read = read;
    }
}

The getter for the title of the article getTitle has been annotated by the @Bindable annotation and notifyPropertyChanged(BR.title) has been added in the setter setTitle. When an article is read, setRead(true) method is called and it adds the “READ” word to the beginning of the original title.

To complete the data class code, a few more lines have to be added. Namely, two OnClickListeners for buttons.

public View.OnClickListener onReadMoreClicked() {
    return new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Toast.makeText(view.getContext(), "Opens article detail", Toast.LENGTH_SHORT).show();
            setRead(true);
        }
    };
}

public View.OnClickListener onCommentsClicked() {
    return new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Toast.makeText(view.getContext(), "Opens comments detail", Toast.LENGTH_SHORT).show();
        }
    };
}

With the data class prepared, let’s head to the most important part of Android data binding — the layout XML file.

Structure of layout files

The structure of Android data binding layout files is very similar to the regular layout XML files. The data binding XML file in addition contains the layout root element followed by the data element and the view root element. The view root element then contains the usual view declarations.

The data section contains variable elements describing properties to be used within your layouts.

<data>
    <variable name="article" type="com.example.databindingblog.Article" />
    <variable name="view" type="android.view.View" />
</data>

Layout expressions are written in the attribute properties using the @{} syntax. Here is an example of setting the text of TextView to the title property of the Article model object.
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/title"
    android:text="@{article.title}" />

A full layout XML file of article list item follows:
<?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">

    <data>
        <variable
            name="article"
            type="com.example.databindingblog.Article" />

        <variable
            name="view"
            type="android.view.View" />
    </data>

    <android.support.v7.widget.CardView
        android:id="@+id/contact_card"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:layout_marginTop="20dp"
        app:cardBackgroundColor="@{article.highlight ? @color/highlight : 0xffffffff}"
        app:cardCornerRadius="3dp"
        app:cardElevation="3dp">

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

            <ImageView
                android:id="@+id/image"
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:layout_alignParentTop="true"
                app:image="@{article.imageUrl}" />

            <TextView
                android:id="@+id/title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignBottom="@+id/image"
                android:layout_alignStart="@+id/image"
                android:layout_marginBottom="10dp"
                android:layout_marginEnd="20dp"
                android:layout_marginStart="20dp"
                android:ellipsize="end"
                android:lines="1"
                android:shadowColor="@android:color/black"
                android:shadowDx="4"
                android:shadowDy="4"
                android:shadowRadius="4"
                android:text="@{article.title}"
                android:textColor="@android:color/white"
                android:textSize="25sp"
                android:textStyle="bold" />

            <TextView
                android:id="@+id/excerpt"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignStart="@+id/image"
                android:layout_below="@+id/image"
                android:layout_marginBottom="5dp"
                android:layout_marginLeft="20dp"
                android:layout_marginRight="20dp"
                android:layout_marginTop="10dp"
                android:lineSpacingMultiplier="1.2"
                android:text="@{article.excerpt}"
                android:textAppearance="?android:attr/textAppearanceSmall" />

            <Button
                android:id="@+id/read_more"
                style="@style/Widget.AppCompat.Button.Colored"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentEnd="true"
                android:layout_below="@+id/excerpt"
                android:layout_marginBottom="10dp"
                android:layout_marginEnd="10dp"
                android:onClick="@{article.onReadMoreClicked}"
                android:padding="10dp"
                android:text="Read more" />

            <Button
                android:id="@+id/comments"
                style="@style/Widget.AppCompat.Button.Colored"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@+id/excerpt"
                android:layout_marginBottom="10dp"
                android:layout_marginEnd="5dp"
                android:layout_toStartOf="@+id/read_more"
                android:onClick="@{article.onCommentsClicked}"
                android:text="@{@plurals/numberOfComments(article.commentsNumber, article.commentsNumber)}"
                android:visibility="@{article.commentsNumber == 0 ? view.GONE : view.VISIBLE}" />

        </RelativeLayout>
    </android.support.v7.widget.CardView>
</layout>

Let’s go from the top of the layout XML file down and describe each usage of Android data binding expression.

First, in the data section, we define two variables: article which is of type com.example.databindingblog.Article and refers to our previously described Article data class; and view of type android.view.View which refers to the Android View class. The latter is used later for accessing the view’s visibility constants in Android data binding expressions.

The first real usage of Android data binding is present in the CardView‘s app:cardBackgroundColor attribute. Using a ternary operator, it is decided whether the article is supposed to be highlighted. Based on the value, we set the colour of the card background. As you can see, the colour can either be represented as a hexadecimal value or defined in Android resources.

app:cardBackgroundColor="@{article.highlight ? @color/highlight : 0xffffffff}"

Next follows an ImageView with app:image attribute. This Android data binding usage expression uses a custom setter which shall be explained in Advanced binding section down below.

The two TextView XML definitions below simply use the property values from Article data object to show a text (article title and excerpt).

android:text="@{article.title}"
android:text="@{article.excerpt}"

Next, there are two buttons, a button referring to a hypothetical article detail labeled “Read more” and a button showing the number of comments and allowing to click-through to see them.

In the XML layout file, we can also define onClick attributes referring to specific implementations in the Article class.

android:onClick="@{article.onReadMoreClicked}"
android:onClick="@{article.onCommentsClicked}"

Android data binding expression language also allows developers to easily use plural string definitions. We use this feature in our comments button which changes its label depending on the count of comments. The android:text attribute then looks like this:
android:text="@{@plurals/numberOfComments(article.commentsNumber, article.commentsNumber)}"

And the numberOfComments plural string is defined in the Android resources as follows:
<plurals name="numberOfComments">
    <item quantity="one">%d comment</item>
    <item quantity="other">%d comments</item>
</plurals>

In case no comments are associated with the article, the comments button is hidden. This is where it comes to the use of the view variable and its constants for view visibility.
android:visibility="@{article.commentsNumber == 0 ? view.GONE : view.VISIBLE}"

One of the nice features worth mentioning is the null coalescing operator ??. If the value of expression before the operator is null, then the following expression value is used. An example follows:
android:text="@{article.title ?? article.no_title}"

The Android data binding library also makes automatically sure to check for nullity of variables and provides defaults (null for String, 0 for int, etc.) if needed.

For a comprehensive list of Android data binding expression language features, please see the official documentation.

Binding the parts together

There are several ways of creating the binding connection. However, the official documentation recommends to create the binding soon after layout inflation. One way is to use the generated Binding class (the full name depends on your layout XML filename — converted to CamelCase and with Binding appended):

MainActivityBinding binding = MainActivityBinding.inflate(layoutInflater);
/* or */
MainActivityBinding binding = MainActivityBinding.inflate(layoutInflater, viewGroup, false);

binding.setArticle(article);

In our example, we use a second approach of binding inside of an adapter using DataBindingUtil‘s static methods:
public class ArticleAdapter extends RecyclerView.Adapter<ArticleAdapter.BindingHolder> {

    private List<Article> mArticles;

    public ArticleAdapter(List<Article> mArticles) {
        this.mArticles = mArticles;
    }

    @Override
    public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ArticleItemBinding binding = DataBindingUtil.inflate(
                LayoutInflater.from(parent.getContext()),
                R.layout.article_item, parent, false);

        return new BindingHolder(binding);
    }

    @Override
    public void onBindViewHolder(BindingHolder holder, int position) {
        ArticleItemBinding binding = holder.binding;
        binding.setArticle(mArticles.get(position));
    }

    @Override
    public int getItemCount() {
        return mArticles.size();
    }

    public static class BindingHolder extends RecyclerView.ViewHolder {
        private ArticleItemBinding binding;

        public BindingHolder(ArticleItemBinding binding) {
            super(binding.contactCard);
            this.binding = binding;
        }
    }
}

Advanced binding

In this section, custom setters and converters will be introduced. These features allow the developer to customize the Android data binding mechanisms to better suit their needs.

Custom setters

You might soon run into the need of custom attributes logic. That’s what BindingAdapters are good for. By using them, the developer can either create new view attributes or override their default behaviour.

One of the most common examples is loading images off the main thread:

<ImageView
    android:id="@+id/image"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:layout_alignParentTop="true"
    app:image="@{article.imageUrl}" />

In the Article data model class, the BindingAdapter method is defined as follows (we use Glide image loading library here):
@BindingAdapter({"image"})
public static void loadImage(ImageView view, String url) {
    Glide.with(view.getContext()).load(url).centerCrop().into(view);
}

Converters

Custom converters can be supplied to transform one type of attribute to another. For example when the attribute expects a Drawable and you want to supply a colour from Android resources. Custom converter could then convert the integer value of colour to ColorDrawable:

<View
   android:background="@{article.highlight ? @color/red : @color/blue}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
   return new ColorDrawable(color);
}

Conclusion

The presented Android data binding possibilities are only a subset of what the library has to provide. However, I hope I was able to mention some of the most common use cases and explained how the Android data binding library works in general. In the next post, I’ll stick to the subject of Android data binding and introduce the MVVM (Model-View-ViewModel) architecture pattern which has been designed to better separate your code and make it testable more easily.

Unfortunately, currently, Android data binding library cannot be used together with the Jack compiler toolchain. That means that for the meantime, you have to chose between data binding and using Java 8 language features, such as lambda expressions or Stream API, both of which I’ve introduced in previous posts.

Here, you can download the whole example application project.

Sources

Share this: