Data Binding Exploration
Quentin Colle
Quentin Colle

Data Binding Exploration

Quentin Colle, Senior Android Engineer

Here at Prolific, we strive to build robust and scalable Android apps for our partners. When the time comes to decide which architecture to adopt, we look at what makes sense for the partner and for the product we are trying to build. First, we want the partner to feel comfortable with our choices. We also want to have a strong foundation to iterate through features and scale easily for future releases.

What About MVVM?

Often, people talk about data binding and MVVM together, as if they were indissociable. While data binding does make a lot of sense to be paired with the MVVM architecture pattern, I would like to emphasize that this is not an obligation: data binding has a lot to offer even when used with traditional MVC (Model View Controller) or MVP (Model View Presenter). Ultimately, what I’m about to present shows you how you can use data binding independently of any architecture pattern you might choose.

This post is not an introduction to data binding; for that example, you can start here. This post is about an exploration of a specific feature of data binding that changed the game for me. I have been testing data binding for about a year now and this is my first time writing about it. I want to explore what the framework has to bring to the table, so let’s dive right in.

Setup

To use data binding, you need to setup your XML layout to use it. Simply wrap any of your layouts with <layout></layout> tags:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
  <data>
    <variable
        name="myText"
        type="String"
        />
  </data>
  <android.support.design.widget.CoordinatorLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      >
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@{myText}" 
        />
  </android.support.design.widget.CoordinatorLayout>
</layout>

By default, a Binding class will be generated based on the name of the layout file, converting it to Pascal case and suffixing “Binding” to it. This class holds all the bindings from the layout variables to the layout’s views. You can use the binding class like this:

public class MainActivity extends AppCompatActivity {
 
  private ActivityMainBinding binding;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Use DataBindingUtil to set the contentView.
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    binding.setMyText("Hello");
  }
}

BaseObservable

Before talking about configuration, it’s important to understand what BaseObservable is. From the documentation:

A convenience class that implements Observable interface and provides notifyPropertyChanged(int) and notifyChange() methods.

First, you reference your BaseObservable class in your layout. You then bind the fields to any views which you can then update by simply calling notifyChange() or notifyPropertyChanged(int) for a specific field.

You can then extract pieces of logic to specific BaseObservable classes and group fields together to have a common entry point to update your UI, and that’s what configurations are all about.

Configurations

Now that we covered the basics, we can talk about the real deal, or what I call “configurations.” Let’s start with an example:

public class ToolbarConfiguration extends BaseObservable {
  // Fields
  private String title;
  private View.OnClickListener listener;
 
  // Bindable getters
  @Bindable public String getTitle() { return title; }
  @Bindable public View.OnClickListener getListener() { return listener; }
 
  // Set content using this method. NotifyChange() will trigger a UI update.
  public void setConfiguration(String title, View.OnClickListener listener) {
    this.title = title;
    this.listener = listener;
    notifyChange();
  }
}

A configuration extends BaseObservable, contains as many parameters as you need, and some setters to update the UI. After that, you add the configuration as a variable inside your XML:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:bind="http://schemas.android.com/apk/res-auto"
    >
  <data>
    <variable
        type="com.prolificinteractive.configurations.binding.ToolbarConfiguration"
        name="config"
        />
  </data>
  <android.support.v7.widget.Toolbar
      android:layout_width="match_parent"
      android:layout_height="?actionBarSize"
      app:title="@{config.title}"
      bind:navigationOnClickListener="@{config.listener}"
      />
</layout>

In your XML, you can then bind parameters from the configuration to any view. You can bind a string title parameter to the app:title field or the listener parameter to the data binding setter setNavigationOnClickListener() of the toolbar.

Finally, all you have to do is call the method that will update the parameters of the configuration by calling notifyChange()In our example, it’s setConfiguration().

public class MainActivity extends AppCompatActivity {
  private ActivityMainBinding binding;
 
  // Configuration
  private ToolbarConfiguration toolbar = new ToolbarConfiguration();
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    binding.setConfig(toolbar); // Bind the configuration to your layout
    toolbar.setConfiguration("My Title", v -> finish()); // Update the UI
  }
}

You can imagine a configuration as a bridge between your business logic and your UI. Configurations allow you to simplify how to display anything UI related by combining correlated components or parameters. In the previous case, I called setConfiguration() with a title and a listener.

You should be able to get things going just with this implementation and already benefit from some of the data binding tools and tricks, but let’s make it better.

Extract and Reference Using the “Include” Tag

One little tweak that I like to do is separate the UI components per configuration. Your code will be cleaner and your components become reusable. All you have to do is import the layout into your screens using includes.

<layout … >
  <data>
    <variable
        type="com.prolificinteractive.configurations.binding.SnackbarConfiguration"
        name="snackbar"
        />
    <variable
        type="com.prolificinteractive.configurations.binding.ToolbarConfiguration"
        name="toolbar"
        />
    <variable
        type="com.prolificinteractive.configurations.binding.ProgressWithTextConfiguration"
        name="progress"
        />
  </data>
  <android.support.design.widget.CoordinatorLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      bind:snackbar="@{snackbar}"
      >
    <include
        layout="@layout/include_toolbar"
        bind:config="@{toolbar}"
        />
    <include
        layout="@layout/include_progress_with_text"
        bind:config="@{progress}"
        />
  </android.support.design.widget.CoordinatorLayout>
</layout>

In this dummy layout I have three configurations — all updating the UI with various parameters — and two of them are paired with includes so they can be reusable anywhere. Another way to look at them is to consider them like custom views.

Now you might wonder, how is the SnackbarConfiguration working with the CoordinatorLayout? The short response is that those configurations work with data binding @BindingAdapter.

BindingAdapters

@BindingAdapter is an annotation supplied by the data binding framework. From the Android documentation:

BindingAdapter is applied to methods that are used to manipulate how values with expressions are set to views. The simplest example is to have a public static method that takes the view and the value to set:

@BindingAdapter("android:text")
 public static void setMoneySign(TextView view, String price) {
     view.setText(SomeMoneyFormatterUtils.format(price));
 }

Although you can actually write logic in your XML files, sometimes the logic is too complex to really fit in it, or you may want to avoid doing so entirely. For example, you could use @BindingAdapter instead.

BindingAdapters can also be used with configurations. Let’s create a configuration for RecyclerView:

public class RecyclerViewConfiguration extends BaseObservable {
  private RecyclerView.LayoutManager layoutManager;
  private RecyclerView.Adapter adapter;
  // etc...
  // We could add an itemAnimator, itemDecoration or any other custom item we need.
 
  @Bindable public RecyclerView.LayoutManager getLayoutManager() { return layoutManager; }
  @Bindable public RecyclerView.Adapter getAdapter() { return adapter; }
 
  public void setConfig(RecyclerView.LayoutManager layoutManager, RecyclerView.Adapter adapter) {
    this.layoutManager = layoutManager;
    this.adapter = adapter;
    notifyChange();
  }
}

This configuration is useful to combine all the dependencies for RecyclerView together.

Here is the BindingAdapter used for the RecyclerViewConfiguration:

@BindingAdapter("configuration")
public static void bindRecyclerViewConfiguration(RecyclerView view, RecyclerViewConfiguration config) {
  if (config != null) {
    if (view.getAdapter() == null && config.getAdapter() != null) {
      view.setAdapter(config.getAdapter());
    }
    if (view.getLayoutManager() == null && config.getLayoutManager() != null) {
      view.setLayoutManager(config.getLayoutManager());
    }
  }
}

Here, we make sure we are not setting a second adapter nor a second LayoutManager. We could potentially have custom behaviors for each of the parameters if needed. Using BindingAdapter gives you way more flexibility to accomplish what you want.

First, in your Java class:

private RecyclerViewConfiguration recyclerView = new RecyclerViewConfiguration();
 
@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
  binding.setRecyclerView(recyclerView);
 
  recyclerView.setConfig(new LinearLayoutManager(this), new MyAdapter());
  adapter.swapData(new ArrayList<>());
}

And second, in your XML:

<layout … >
  <data>
    <variable
        type="com.prolificinteractive.configurations.binding.configurations.RecyclerViewConfiguration"
        name="recyclerView"
        />
  </data>
  <android.support.v7.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    bind:configuration="@{recyclerView}"
    />
</layout>

Polishing Using the Builder Pattern

As I was experimenting with configurations, I came across one major issue. Often, you have to display screens for empty states, i.e., if you get an empty list of products or if your API request returns an error. I call those views “empty views.” These views generally look alike to keep a common feel across the entire app. Usually, the content and behavior of those empty views are the only part differentiating one from another. Normally, without data binding, I would most likely build a custom view with multiple TextViews and some setters to update the content of those views. With data binding, however, this is a perfect use case for configuration.

So I wrote a first version of EmptyViewConfiguration:

public class NotSoGoodEmptyViewConfiguration extends BaseObservable {
  private String title;
  // … All your fields here
 
  @Bindable public String getTitle() { return title; }
  // … All your bindable getters here
 
  // Set fields and update the UI
  public void setConfig(String title, String subtitle, String button, View.OnClickListener listener) {
    this.title = title;
    this.subtitle = subtitle;
    this.button = button;
    this.listener = listener;
    notifyChange();
  }
}

Then you can set the content by calling it like this:

// Declaration
NotSoGoodEmptyViewConfiguration emptyView = new NotSoGoodEmptyViewConfiguration();
// Usage
emptyView.setConfig("Oh Oh!", "No products found...", "Try Again", v -> tryAgain());

This version of EmptyViewConfiguration brings up two issues:

The first is that, in some cases, you might want to have only a title and a subtitle but no button. In other cases, just a button and a title. If you add specific methods in your configuration for each case, it quickly gets really out of hand when you have multiple parameters.

// No subtitle
public void setConfig(String title, String button, View.OnClickListener listener) {
  this.title = title;
  this.button = button;
  this.listener = listener;
 
  notifyChange();
}
 
// No button
public void setConfig(String title, String subtitle) {
  this.title = title;
  this.subtitle = subtitle;
 
  notifyChange();
}
 
// etc
...

The second issue is that you might have two different empty view states (empty list and API error for example) in the same activity, and you can use the same EmptyViewConfiguration for both. Basically, you just set the content by calling the setConfig() method for whichever state you are in. The issue comes up when you are updating different parameters for each state. For example, if you use setConfig(button, listener) in state 1, followed by another state 2 which uses setConfig(title, subtitle), you might end up with something completely wrong on the screen if you have not been setting previously used parameters to null.

To mitigate those issues, I found that the best solution is to use a builder pattern. Here is the final version of my EmptyViewConfiguration:

// Empty view configuration must be used using the builder.
public class EmptyViewConfiguration extends BaseObservable {
  private String title;
  // Fields
 
  @Bindable public String getTitle() { return title; }
  // Bindable getter methods
 
  // Insert any mandatory parameters directly to this method and to the constructor
  public Builder newState() { return new Builder(); }
 
  public class Builder {
    private String subtitle;
    private String title;
    private String button;
    private View.OnClickListener listener;
 
    private Builder() { }
 
    public Builder setTitle(String title) {
      this.title = title;
      return this;
    }
 
    // … Insert all the setters here
    
    public void commit() {
     setConfig(title, subtitle, button, listener);
    }
  }
 
  // Private! One method, only accessible from the builder.
  private void setConfig(String title, String subtitle, String button, View.OnClickListener listener) {
    this.title = title;
    this.subtitle = subtitle;
    this.button = button;
    this.listener = listener;
 
    notifyChange();
  }
}

And usage in your activity:

// Title and Subtitle only
emptyView.newState().setTitle("Oh Oh!").setSubtitle("No products found...").commit();
// Button only
emptyView.newState().setButton("Try Again", tryAgain()).commit();

The XML stays the same.

Even though it’s not mandatory, using a builder really simplifies the usage of those configurations and prevents some issues.

TL;DR

Configurations make use of the data binding BaseObservable object to get around custom views and default UI updates for views through a single point of entry. Configurations are highly customizable and often simplify your layouts, using includes layouts, while making them reusable across multiple activities, fragments, etc.

Data binding — and more specifically configurations — are useful when combined with MVVM architecture pattern, but can absolutely be used in any other architecture, like a more traditional MVP (or even MVC!?).

A configuration is best paired to a Builder pattern and can even be used with BindingAdapters or anything else that the data binding framework supports.

For concrete examples and more, make sure to check this GitHub repo.