Android Data Binding: Write Less to do More

0
6581
Android Application Development

Main

The Android Data Binding framework leads to a significant reduction in code and, as a bonus, comes with other goodies such as BindingAdapters and Auto-Magic attributes, at almost no performance cost.

Let’s face it, we all love Android, but we don’t really like one of the side-effects of Android development, which is boilerplate code. JAVA is a verbose language, and when it comes to working with a UI framework, the code that is written to essentially glue your UI to your model is really painful. The code to glue the UI to your model is important, but is something that can be eliminated with technology. The simple steps for gluing the UI to the model in Android can be as follows:

  • First, get a reference to the view with findViewById()
  • Next, load the model from the disc or over the network
  • Then load the data from the model into the view; for example, setting text in TextView or loading an image in ImageView
  • Now, if any changes happen in the UI which affect the model, update the model
  • And if the changed value is displayed elsewhere in the UI, update those views too

Now, this isn’t the only way we can bind the model to the UI, but do realise that we’re writing fair amounts of code just to get our data to the view.

Data binding
Data binding, in general: A typical definition from Wikipedia states that data binding is a general technique that binds data sources from the provider and consumer together, and synchronises them. What this means descriptively is that data binding is a technique in which the UI and the model are connected in such a way that the UI as well as the model generate proper notifications about data changes, and use those notifications to keep each other updated about data.
Types of data binding: There are typically two types of data binding. The first is one-way data binding and the other is two-way data binding. These operate pretty much as their names suggest. One-way data binding is when data flows in only one direction, which is, from the model or the data to the UI. An example of it can be when a TextView sets its data from a variable and keeps the text of the view updated with the variable whenever the variable changes.
With two-way data binding, the data flows in both directions, i.e., from the model to the UI and from the UI to the model. An example would be TextView and EditText — the TextView shows the contents of EditText. Now, if I bind EditText and TextView with the model in two-way binding, when we write some text in EditText, the data of EditText is passed to the model or variable, which is the flow from UI to model. When the variable is updated, it is bound with TextView, so the contents of TextView are also updated, and this flow is from the model or the variable to the UI. This is how data flows in two ways; hence, the name two-way data binding. It’ll all make more sense when we get to the code.

Writing the first app
First, you need to make your project data binding-enabled. You do that by adding the following code to the build.gradle file for the module of your app, and not to the global build.gradle file for the whole project. Note that this is the way to enable data binding in Studio 2.0 and later. You can search on the Internet about enabling it in versions below 2.0.

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

Now, let’s consider a simple model named User. It contains two fields — the first name and last name.

public class UserData {
private String mFirstName;
  private String mLastName;
 
public UserData(String firstName, String lastName) {
mFirstName = firstName;
mLastName = lastName;
}
…
…
//getters and setters
}

This is a very basic model. Let’s take a look at the layout file for our activity.

<layout
xmlns:android=”http://schemas.android.com/apk/res/android”
   xmlns:app=”http://schemas.android.com/apk/res-auto”
   xmlns:tools=”http://schemas.android.com/tools”>
 
   <data>
       <variable
           name=”user”
           type=”in.samvidinfotech.bindingdemo.UserData” />
   </data>
 
   <LinearLayout
       android:orientation=”vertical”
       android:layout_width=”match_parent”
       android:layout_height=”match_parent”
       android:gravity=”center”> 
       <TextView
           android:text=”@{user.firstName}”
           tools:text=”First Name”
           android:layout_width=”wrap_content”
           android:layout_height=”wrap_content”/>
       <TextView
           tools:text=”Last Name”
           android:text=”@{user.lastName}”
           android:layout_marginTop=”10dp”
           android:layout_width=”wrap_content”
           android:layout_height=”wrap_content”/>
 
   </LinearLayout>
</layout>

Our layout file is where all the magic happens. The essence of the data binding framework on Android is that it lets us put expressions in our XML code. Now, to indicate that a layout file is using data binding, we have the <layout> tag at the top of our view hierarchy. The layout inside LinearLayout is our normal activity layout. The <data> tag contains information about the data being used inside the current XML file. So, here we declare a variable in the <variable> tag. The name attribute defines the name to be used in the layout file to refer to the variable, and the type attribute determines the type of the class. We can have many variables inside a single layout file.
Now let’s look at the code of activity to set the data in the layout file:

protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   ActivityUserBinding activityUserBinding = DataBindingUtil.setContentView(this, R.layout.activity_user);
 
   activityUserBinding.setUser(new UserData(“Test”, “User”));
}

Pretty neat, right? The code to set the data in our layout file is only two lines. Now, where did the ActivityUserBinding class come from? The data binding framework generates compile time classes based on our layout files, which lets the data binding framework work without using any reflection at runtime. The name of these classes is given by converting the name of the layout file to ‘camel case’ and appending binding to it. For instance, the layout name activity_user.xml becomes ActivityUserBinding. These classes live in the data binding package inside your application, i.e., <your-package-name>.databinding.ActivityUserBinding. You can also change the place where the classes are stored.

Figure 1
Figure 1: RecyclerView with data binding

Writing observable models
Currently, if you run your application, it will set the data in the activity, but it won’t update the data in the UI if the data in your model changes. In order to do that, we need to make our models smarter — we need to make them observable. The easiest way to do that would be to extend the BaseObservable class; it provides convenience methods for notifying you about the data changes. Now, if we change our UserData model to be an observable model, it will look like what’s shown below:

public class UserData_BaseObservable extends BaseObservable {
   @Bindable
   private String mFirstName;
   @Bindable
   private String mLastName;
 
   public UserData_BaseObservable(String firstName, String lastName) {
       mFirstName = firstName;
       mLastName = lastName;
   }
 
   public String getFirstName() {
       return mFirstName;
   }
 
   public void setFirstName(String firstName) {
       mFirstName = firstName;
       notifyPropertyChanged(in.samvidinfotech.bindingdemo.BR.firstName);
   }
 
   public String getLastName() {
       return mLastName;
   }
 
   public void setLastName(String lastName) {
       mLastName = lastName;
       notifyPropertyChanged(in.samvidinfotech.bindingdemo.BR.lastName);
   }
}

The BaseObservable class makes our model observable for data changes. But the data binding class cannot just magically know when the data in the model changes; so we need to inform the framework when the data changes. To do that, we need to add the @Bindable annotation to the fields that we want observed. So whenever that property changes, we notify the framework with the convenience method notifyPropertyChanged(), which the BaseObservable class gives us. We pass the field, which is changed as argument in call to notifyPropertyChanged(). Here, we have a BR class, which contains constants of fields that are observable inside the whole application. This BR class is similar to the R.java class that keeps all of our resources’ IDs. So our variables are observables; we have our observable fields as constants in the BR class. Note that if we use data binding, we can update our data from any thread, and data binding will take care of updating the data from the UI thread when the next frame comes.
Now, if you have your models extending from any other base class, or if for any other reason you can’t extend the BaseObservable class, then you can implement an observable interface directly. The BaseObservable class itself implements Observable internally. So, if we switch to Observable in our UserData model, it would look something like what’s shown below:

public class UserData implements Observable {
   private PropertyChangeRegistry mPropertyChangeRegistry;
   private String mFirstName;
   private String mLastName;
 
   public UserData(String firstName, String lastName) {
       mFirstName = firstName;
       mLastName = lastName;
       mPropertyChangeRegistry = new PropertyChangeRegistry();
   }
 
   public void setFirstName(String firstName) {
       mFirstName = firstName;
       mPropertyChangeRegistry.notifyChange(this, in.samvidinfotech.bindingdemo.BR.firstName);
   }
 
   public void setLastName(String lastName) {
       mLastName = lastName;
       mPropertyChangeRegistry.notifyChange(this, in.samvidinfotech.bindingdemo.BR.lastName);
   }
 
   @Bindable
   public String getFirstName() {
       return mFirstName;
   }
 
   @Bindable
   public String getLastName() {
       return mLastName;
   }
 
   @Override
   public void addOnPropertyChangedCallback(OnPropertyChangedCallback onPropertyChangedCallback) {
       mPropertyChangeRegistry.add(onPropertyChangedCallback);
   }
 
   @Override
   public void removeOnPropertyChangedCallback(OnPropertyChangedCallback onPropertyChangedCallback) {
       mPropertyChangeRegistry.remove(onPropertyChangedCallback);
   }
}

Here, we need to create an object of PropertyChange Registry, and we need to add and remove callbacks from it using the methods addOnPropertyChangedCallback and removeOnPropertyChangedCallback, respectively, which the observable interface gives us. You may notice that rather than adding the @Bindable annotation to variables, we have added it to getter methods, which is also a way of making our fields observable.

Working with lists
When it comes to working with lists, data binding is extremely helpful, while also being very easy to integrate. You can easily create a layout file, as shown earlier, for a list item. Then, in the adapter for your RecyclerView, you can store the binding in your ViewHolder, and in onBindViewHolder(), you just set the variable in binding. The code for ViewHolder is pretty small and neat.

public class UserViewHolder extends RecyclerView.ViewHolder {
   public UserViewBinding mBinding;
 
   public UserViewHolder(UserViewBinding viewBinding) {
       super(viewBinding.getRoot());
       mBinding = viewBinding;
   }
}

Now, when you create a ViewHolder…

@Override
public UserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
   return new UserViewHolder(UserViewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
}

…you may be wondering where the inflate method came from. Note that when our binding classes are generated, they are given these convenient static methods for easy initialisation. Now, in onBindViewHolder, you just set the variable.

@Override
public void onBindViewHolder(UserViewHolder holder, int position) {
   holder.mBinding.setUser(mUserDatas.get(position));
   holder.mBinding.executePendingBindings();
}

And with almost this much code, you can come up with something like the screenshot in Figure 1. Be sure to executePendingBindings(); otherwise, RecyclerView would do two layout passes.

Binding adapters
Binding adapters are one of the coolest things that the data binding framework can do. If you want to override the functionality of what an attribute does or want to introduce your own XML attribute to do some work, binding adapters can be of help. For instance, if you want to set an image in your ImageView in XML, but the image is a URL for a resource on your server, the URL of the image is stored in the model you set on your layout file. So binding adapters can help you here.

@BindingAdapter(“imageUrl”)
public static void setImage(ImageView image, String url) {
   Glide.with(image.getContext()).load(url).crossFade().centerCrop().into(image);
}

You can create binding adapter methods by annotating them with the @BindingAdapter method you pass in the attribute you want to override. In method parameters, the first parameter should be the type of view you want this annotation to be on, and the other should be the type of value in the parameter that you set in the annotation. Then, inside the method, you can do anything with the parameter and the view. This is how a simple binding adapter can be written. If you want to do something a little bit more complex, like passing multiple parameters in binding adapters, that can also be done, as follows:

@BindingAdapter(value = {“imageUrl”, “placeholder”}, requireAll = false)
public static void setImage(ImageView image, String url, Drawable placeholder) {
   Glide.with(image.getContext()).load(url).placeholder(placeholder).crossFade().centerCrop().into(image);
}

You just need to take care that the ordering of the values for attributes and the value types match the types of parameters; otherwise, your method may not be called.

 

LEAVE A REPLY

Please enter your comment!
Please enter your name here