Android data binding cover?fm=jpg&fl=progressive&q=75&w=300

Android Data Binding

Get it straight from the horseā€™s mouth: Step one: use Data Binding in Android. Step two: profit šŸ’°. Yigit Boyar and George Mount are Google developers who helped build Androidā€™s Data Binding Library to allow developers to build rich & responsive user experiences with minimal effort. In this talk at the Bay Area Android Dev Group, they demonstrate how using Data Bindings can improve your application by removing boilerplate for data-driven UI, allowing you to write cleaner, better code.


Introduction (0:00)

We are George Mount and Yigit Boyar, and we work on the Android UI Toolkit team. We have a lot of information about Data Binding to share with you, and lots of code to go with it. Weā€™ll discuss the important aspects of how Data Binding works, how to integrate it into your app, how it works with other components, and weā€™ll mention some best practices.

Why Data Binding? (0:44)

You may wonder why we decided to implement this library. Hereā€™s an example of a common use case.

<LinearLayout ā€¦>
    <TextView android:id="@+id/name"/>
    <TextView android:id="@+id/lastName"/>
</LinearLayout>

This is an Android UI you see all the time. Say you have a bunch of videos with IDs. Your designer comes and says, ā€œOkay, letā€™s try adding new information to this layout,ā€ so that when you add any video, you need to tack on another ID. You go back to your Java code in order to modify the UI.

private TextView mName
protected void onCreate(Bundle savedInstanceState) {
  setContentView(R.layout.activity_main);
  mName = (TextView) findViewById(R.id.name);
}

public void updateUI(User user) {
  if (user == null) {
    mName.setText(null);
  } else {
    mName.setText(user.getName());
  }
}

You write a new TextView, you find it from the UI, and you set your logic so that whenever you need to update your user, you have to set the information on the TextView.

private TextView mName
protected void onCreate(Bundle savedInstanceState) {
  setContentView(R.layout.activity_main);
  mName = (TextView) findViewById(R.id.name);
  mLastName = (TextView) findViewById(R.id.lastName);
}
public void updateUI(User user) {
  if (user == null) {
    mName.setText(null);
    mLastName.setText(null);
  } else {
    mName.setText(user.getName());
    mLastName.setText(user.getLastName());
  }
}

All in all, that is a lot of things you have to do just to add one view to your UI. It seems like too much stupid boilerplate code that doesnā€™t require any brainpower.

There are already some really nice libraries to make this easier and more solid. For example, if you use ButterKnife, you could get two of those ugly viewByIds, making it much easier to read. You can get rid of the extra code, telling ButterKnife to delete it for you.

private TextView mName
protected void onCreate(Bundle savedInstanceState) {
  setContentView(R.layout.activity_main);
  ButterKnife.bind(this);
}
public void updateUI(User user) {
  if (user == null) {
    mName.setText(null);
    mLastName.setText(null);
  } else {
    mName.setText(user.getName());
    mLastName.setText(user.getLastName());
  }
}

Itā€™s a good step forward, but we can go one step further. We can say ā€œOkay, why do I need to create items for these? Something can just generate it. I have a layout file, I have IDā€™s.ā€ So you can use Holdr, which does that for you. It processes your files and then creates views for them. You initiate from Holdr, which converts the IDs you entered into field names.

private Holdr_ActivityMain holder;
protected void onCreate(Bundle savedInstanceState) {
  setContentView(R.layout.activity_main);
  holder = new Holdr_ActivityMain(findViewById(content));
}
public void updateUI(User user) {
  if (user == null) {
    holder.name.setText(null);
    holder.lastName.setText(null);
  } else {
    holder.name.setText(user.getName());
    holder.lastName.setText(user.getLastName());
  }
} 

This is better again, but thereā€™s still something unnecessary in this code. Thereā€™s a huge part that I never touched, where I was unable to reduce the amount of code. Itā€™s all very simple code, too: I have a user object, I just want to move the data inside of this object to the view class. How many times have you made a mistake when you see code like this? You remember to change one thing, but forget to change another, and end up with a crash on production. This is the part we want to focus on: we want to get through all the boilerplate code.

When you use Data Binding, itā€™s very similar to using Holdr, but you have to do a lot less work. Data Binding figures the rest out.

private ActivityMainBinding mBinding;
protected void onCreate(Bundle savedInstanceState) {
  mBinding = DataBindingUtil.setContentView(this,
                           R.layout.activity_main);
}

public void updateUI(User user) {
  mBinding.setUser(user);
}

Behind the Scenes (3:53)

How does Data Binding work behind the scenes? Take a look at the layout file from before:

<LinearLayout ā€¦>
  <TextView android:id="@id/name"    />
  <TextView android:id="@id/lastName"    />
</LinearLayout>

I have these IDs, but why do I need them if I could find them back in my Java code? I actually donā€™t need them anymore, so I can get rid of them. In their place, I put the most obvious thing I want to display.

<LinearLayout ā€¦>
  <TextView android:text="@{user.name}"/>
  <TextView android:text="@{user.lastName}"/>
</LinearLayout>

Now, when I look at this layout file, I know what the TextView shows. It has become very obvious, so I donā€™t need to go back to read my Java code. We designed the Data Binding library in a way that didnā€™t include any magic that wasnā€™t easy to explain. If you are using something in your layout file, you need to tell Data Binding what it is. You simply say, ā€œWe are labeling this layout file with this type of user, and now we are going to find it.ā€ If your designer asks you to add another view, you simply add one more line and show your new view, with no other code changes.

<layout>
    <data>
        <variable name="user"
                  type="com.android.example.User"/>
    </data>
    <LinearLayout ā€¦>
        <TextView android:text="@{user.name}"/>
        <TextView android:text="@{user.lastName}"/>
        <TextView android:text='@{"" + user.age}'/>
    </LinearLayout>
</layout>

Itā€™s also really easy to find bugs. You can look at something like the above code and and say, ā€œOh, look! Empty string plus user.age!ā€ You just set text on the integer, and then bang! We did that many times, it just happens.

But How Does It Work? (5:57)

The first thing the Data Binding library does is process your layout files. By ā€œprocess,ā€ I mean that it goes into to your layout files when your application is being compiled, finds everything about Data Binding, grabs that information and deletes it. We delete it because the view system doesnā€™t know about it, so it disappears.

Get more development news like this

The second step is to parse these expressions by running it through a grammar. For example, in this case:

<TextView android:visibility="@user.isAdmin ? View.VISIBLE : View.GONE}"/>

The user is an ID, the View is an ID, and the other View is an ID. Theyā€™re identifiers, like real objects, but we donā€™t really know what they are yet at this point. The other things are invisible or gone. There is field access, and the whole thingā€™s a ternary. Thatā€™s what we have understood so far. We parse things from a file, and understand whatā€™s inside.

The third step is resolving dependencies, which happens when your code is being compiled. In this step, for example, we look at user.isAdmin and figure out what it means. We think ā€œOkay, this method turns a boolean inside that user class. I know this expression means some sort of boolean at run time.ā€

The final step is writing data binders. We write the classes that YOU donā€™t need to write anymore. In short, final step: profit šŸ’°

An Example Case (7:40)

Here is an actual case of a layout file.

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="user" type="com.android.example.User"/>
    </data>
   <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        <TextView android:text="@{user.name}"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"/>
        <TextView android:text="@{user.lastname}"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"/>
    </RelativeLayout>
</layout>

As we process, we get rid of everything the view system doesnā€™t know anymore, link them, and put back our binding tags:

   <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        <TextView android:tag="binding_1"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"/>
        <TextView android:tag="binding_2"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"/>
    </RelativeLayout>

This is actually how we make Data Binding backwards compatible. When you put it on a Gingerbread device, the poor guy has no idea whatā€™s going on.

Expression Tree (8:01)

<TextView android:text="@{user.age < 18 ? @string/redacted : user.name}"/>

Hereā€™s another example expression. When we parse this, it turns into an expression tree which is resolved at compile time. Itā€™s important to note that it happens in the compile time, so that when the application starts running, you already know everything. We check the left side of this expression, and itā€™s a boolean. We check the right side, and itā€™s a string. The resource is also a string. So I have a boolean, string, string, ternary, which is also a string. Thereā€™s a text attribute and I have a string. How do I set this?

Thereā€™s a perfect setText(CharSequence). Now, Data Binding knows how to turn that expression into Java code. If you go into detail, thereā€™s TextView and ImageView.

<TextView android:text="@{myVariable}"/>
textView.setText(myVariable);
<ImageView android:src="@{user.image}"/>
imageView.setSrc(user.image);

ImageView is a source attribute, so would it be correct, as in the above example, to use setSrc? No, because thereā€™s no set source method on ImageView. Instead, thereā€™s an inside ImageView source method. But how does Data Binding know about this?

Itā€™s called source attribute, and since youā€™re used to using that attribute, Data Binding has to support it.

<TextView ā€¦/>
textView.setText(myVariable);
<ImageView android:src="@{user.image}"/> 
imageView.setImageResource(user.image);
          @BindingMethod(
              type = android.widget.ImageView.class,
              attribute = "android:src",
              method = "setImageResource") 

We have these annotations that we create, where you can simply say, ā€œOkay, in the ImageView class, attribute source maps to this method.ā€ We just write it once, we actually form the framework once. We provide it, but you may have custom views that you want to add. Once you add that method, Data Binding knows how to resolve this. Again, this all happens in the compile time.

Data Binding Goodies (9:54)

Data Binding makes your life a lot easier. Letā€™s take a look at the expression language that we support, which is mostly Java. It allows things like field access, method calls, parameters, addition, comparisons, index access on arrays, constant access, and even ternary expressions. Thatā€™s basically what you want from your Java expressions. There are also a few things it doesnā€™t do, like new. We really donā€™t want you to do new in your expressions.

Our basic goal is to make this thing as short and readable as possible in your expressions, right in your XML. We donā€™t want you to have to write super long expressions just to access your contactā€™s name. We want you to be able to use contact.name. We look at it and think ā€œOkay, is this a field, or is it a getter?ā€ Or you could have ā€œnameā€ as a method.

We also do automatic null checks, which is actually really, really cool. If you want to access the name, but contact is null, how much of pain in the neck would it be to write contact null ? null : contact.friend null ? :? You donā€™t want to do that. Now, if contact is null, the whole expression in null.

We also have the null coalescing operator, which you may have seen from other languages. Itā€™s just a convenient way to do this ternary operator:

contact.lastName ?? contact.name
contact.lastName != null ? contact.lastName : contact.name

It says if the first one is not null, choose the first one. If it is null, then choose the second one.

We also have list access and map access using the bracket operator. If you have contacts[0], that contact could be a list or an array, itā€™d be fine. If you have contactInfo, you can use a bracket notation for that. Itā€™s a little easier.

Resources (12:20)

We want you to be able use resources in your expressions. What would an expression language be in Android without resources? Now you can use resources and string formatting right in your expressions.

In Expressions:

android:padding="@{isBig ? @dimen/bigPadding : @dimen/smallPadding}"

Inline string formatting:

android:text="@{@string/nameFormat(firstName, lastName)}"

Inline plurals:

android:text="@{@plurals/banana(bananaCount)}"

Automagic Attributes (13:00)

Here we have a DrawerLayoutā€¦

<android.support.v4.widget.DrawerLayout
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  app:scrimColor="@{@color/scrim}"/>
drawerLayout.setScrimColor(
  resources.getColor(R.color.scrim))

We have this attribute app:scrimColor. Thereā€™s no scrim color on the DrawerLayout, but there happens to be setScrimColor. We look for this setScrimColor when we have an attribute with a name scrimColor, and we check if the types match. First we look at color, which is an int. If setScrimColor takes an int, itā€™s a match. Itā€™s convenient!

Event Handlers (13:41)

I donā€™t know how many of you have done clicked using a button or a view, but we also support it here in Data Binding. You can use a clicked, but now any of the events are supported as well. Of course, this works back to Gingerbread. You can even do things where you have assigned an arbitrary event handler as part of an expression (Iā€™m not saying I recommend you do this, but you can!). You can also do some of the weird listeners, like onTextChanged. TextWatcher has three methods on it, but everybody only cares about onTextChanged, right? You can actually access just one of them if you want, or all of them.

<Button android:onClick="clicked" ā€¦/>

<Button android:onClick="@{handlers.clicked}" ā€¦/> 

<Button android:onClick="@{isAdult ? handlers.adultClick : handlers.childClick}" ā€¦/> 

<Button android:onTextChanged="@{handlers.textChanged}" ā€¦/>

Observability in Detail (14:56)

What happens when you update your view? Imagine we have a store, and we have an item whose price has recently changed. This has has to automatically update our UI. How does that happen? With Data Binding, that happens really cheaply and easily.

The first thing we have to do is create an item, some kind of observable object. Here, Iā€™ve extended the base observable object, and then we have our fields in there.

public class Item extends BaseObservable {
    private String price;

    @Bindable
    public String getPrice() {
        return this.name;
    }

    public void setPrice(String price) {
        this.price = price;
        notifyPropertyChanged(BR.price);
    }
}

We notify it by adding in this notifyPropertyChanged. But what do we notify thatā€™s going to change? We have to put in a @Bindable annotation on the getPrice. That generates this BR.price, the price field in the BR class. The BR is like the R class, we just generate it for you and it just sucks in these binding resources. However, you may not want us to invade your whole hierarchy, so we allow you to implement the observable class as well. Yes, I hear the has-a vs. is-a people complainingā€¦ Here we allow you to implement it yourself.

public class Item implements Observable {
    private PropertyChangeRegistry callbacks = new ā€¦
    ā€¦
    @Override
    public void addOnPropertyChangedCallback( 
            OnPropertyChangedCallback callback) {
        callbacks.add(callback);
    }
    @Override
    public void removeOnPropertyChangedCallback( 
            OnPropertyChangedCallback callback) {
        callbacks.remove(callback);
    }
}

We have this convenient class called the PropertyChangedRegistry that lets you essentially take those callbacks and notify them. Some of you might think this is just a pain in the neck, and instead want to have an observable field. Essentially, each of these is an observable object, and it just has one entry in it. Itā€™s conveniently set up so that when you use, say, accessImage, it actually accesses the content within that image. If you access price, it accesses the string content of that price.

The special thing about these objects is that in the Java code, you have to call the set and get methods, but in your binding expressions, you can just say item.price, and we will know that we need to call the getter. So when the price changes, it just sets the value.

public class Item {
    public final ObservableField<Drawable> image =
            new ObservableField<>();
    public final ObservableField<String> price =
            new ObservableField<>();
    public final ObservableInt inventory =
            new ObservableInt();
}

item.price.set("$33.41");

In other cases you may have more ā€œblobbyā€ data. This often happens at the beginning of your development cycle, specifically during prototyping, where you might have some blob that comes down from the net, and you donā€™t really want to define objects quite yet, so you might want a map. In this case, all you have to do is have an observable map into which you can put your items, and then you can access them. Unfortunately, you donā€™t access it the same way exactly: you have to use the bracket notation.

ObservableMap<String, Object> item =
        new ObservableArrayMap<>();

item.put("price", "$33.41"); 
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" 
    android:text='@{item["price"]}'/>

Notify on Any Thread (18:29)

One of the conveniences here is that you donā€™t have to notify on the UI thread anymore: you can update on any thread you want. However, note that we are going to read on the UI thread, so you have to be careful about that. Also, please donā€™t do this with lists! For lists, you should still notify on the UI thread, because we are going to read on the UI thread, and we are going to need the length on the UI thread, and we do not do any kind of synchronization on that. You probably already know this from Recycler and ListView, which get very upset. Itā€™s because of lists, not because of those classes.

Performance (19:21)

Perhaps the most important consideration for this project was to not make it slow. Data Binding is infamous for being slow, so in Android, we were double careful to take that into consideration, and we believe we did a good job.

The foremost aspect of performance is that there is basically zero reflection. Everything happens in compile time. Occasionally, things are inconvenient because it happens in the compile time, but in the long run we donā€™t care. We donā€™t want to have to resolve anything when the application is running.

The second part is something nice that you get for free. Letā€™s say you are using Data Binding in a layout where you name the price of an object, but then the price of an object changes. So new price comes, the notify comes. Data Binding is only going to update the TextView, nothing else, however that TextView will be measured. If you were writing that code by hand, itā€™s very unlikely that you would write that code, and it would just set off the view again. So this comes with a free benefit.

Another performance benefit in Data Binding comes in cases where you have two expressions such as these:

<TextView android:text="@{user.address.street}"/>
<TextView android:text="@{user.address.city}"/>

You have user.address and another user.address. The code DataBinding will generate looks like this:

Address address = user.getAddress();
String street = address.getStreet();
String city = address.getCity();

Itā€™s going to move the address into a local variable, then operate on it. Now imagine that thereā€™s some calculation, which is actually expensive. Data Binding is only going to do it once. Itā€™s yet another thing that you wouldnā€™t do by hand.

Another positive side effect of the performance is the findById. When you code findById on the view on Android, it actually goes to all of its children, and says something like ā€œChildren zero, can you find this view by ID?ā€ That child asks its children, which then go to the next children, etc. until you find the view. Then, you code findViewById a second time for the other view, and the same thing happens again.

However, when you initialize Data Binding, we actually know which views we are interested in at compile time, so we have a method of finding all the views we want. We traverse the layout hierarchy once to collect all the views. Itā€™s the same story, we traverse it, but it only happens once. The second time we need another view, thereā€™s no second pass, because we already found all the views.

Performance is about the little details. Youā€™re including a library in your code, so yes, some behaviors will change, and yes, there will be some cost. But with all these things, I think we made it equal to, even sometimes better than the code you would write, which is very important.

RecyclerView and Data Binding (22:14)

Using ViewHolders was very common for ListViews, and RecyclerView enforces this pattern. If you look at what Data Binding generates, youā€™ll see that it actually generates the ViewHolder for you. It has the fields, it knows the views. You can also easily use the inside of RecyclerView. We create a ViewHolder that has this basic create method, a static method, which tells the UserItemBinding (the generated class from a user item layout file). So you call UserItemBinding inflate. And now you have a very simple ViewHolder class that just keeps a reference to the binding that was generated, and the binding method likes this.

public class UserViewHolder extends RecyclerView.ViewHolder {
  static UserViewHolder create(LayoutInflater inflater, ViewGroup parent) {
    UserItemBinding binding = UserItemBinding
        .inflate(inflater, parent, false);
    return new UserViewHolder(binding);
  } 
  private UserItemBinding mBinding;
  private UserViewHolder(UserItemBinding binding) {
    super(binding.getRoot());
    mBinding = binding;
  }
  public void bindTo(User user) {
    mBinding.setUser(user);
    mBinding.executePendingBindings();
  }
}

One little detail to be careful about is to call this executePendingBindings. This is necessary because when your data invalidates, Data Binding actually waits until the next animation frame before it sets the layout. This is not so that we can batch all the changes that happen in your data and apply all it once, but because RecyclerView doesnā€™t really like it. RecyclerView calls BindView, it wants you to prepare that view so that it can measure a layout. This is why we call executePendingBindings, so that Data Binding flushes all pending changes. Otherwise, itā€™s going to create another layout invalidation. You may not notice it visually, but itā€™s going to be on the list of operations.

For onCreateViewholder, it simply calls the first method, and onBind passes the object to the ViewHolder. Thatā€™s it. We didnā€™t write any findViewById, no settings, nada. Everything is encapsuled in your layout file.

public UserViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
  return UserViewHolder.create(mLayoutInflater, viewGroup);
}

public void onBindViewHolder(UserViewHolder userViewHolder, int position) {
  userViewHolder.bindTo(mUserList.get(position));
}

In the previous code, we showed a very simple, straightforward implementation. Say, for instance, that your user objectā€™s name changed. The binding system is going to realize it and re-layout itself in the next animation frame. The next animation frame starts, calculates what has changed, and updates the TextView. Then, TextView says, ā€œOkay, my text has changed, I have to re-case the layout now because I donā€™t know my new size. Letā€™s go tell RecyclerView that one of its children is unhappy, and it needs to re-layout itself too.ā€ When this happens, youā€™re not going to receive any animations because you told RecyclerView after everything happened. RecyclerView will try to fix itself, it will be done. Result: NO ANIMATIONS But thatā€™s not what we wanted.

What we wanted to happen was that when the userā€™s object is invalidated, we tell the adapter the item has changed. In turn, it is going to tell the RecyclerView, ā€œHey, one of your children is going to change, prepare yourself.ā€ RecyclerView will layout, and for those whose children have changed, itā€™s going to instruct them to rebind themselves. When they rebind, TextView will say, ā€œOkay, my text is set, I need the layout.ā€ RecyclerView will say, ā€œOkay, donā€™t worry, Iā€™m already laying you out, let me measure you.ā€ Result: MUCHO ANIMATIONS. You will get all the animations because everything happened under the control of RecyclerView.

Rebind Callback and Payload (25:50)

This is actually the part we need to release as a library, but in the meantime, I want to let you know how you can do this. In Data Binding, we have this API where you can add a rebind callback. Itā€™s basically a callback you can attach and then get notified when Data Binding is about to calculate. For instance, maybe you may want to freeze the changes to the UI. You can just hook to this onPreBind, at which point you get to return a boolean where you can say, ā€œNo, donā€™t rebind yet.ā€ If one of the listeners says that, Data Binding is going to call, ā€œHey, I canceled the rebind. Now itā€™s your responsibility to call me, because Iā€™m not going to do anything.ā€

Now all we have to do here is if RecyclerView is not calculating your layout, return false. View, you should not update yourself when RecyclerView is not doing your computation. That is computing layouts, the new RecyclerView API that was released this summer. And when the onCanceled comes, we just tell the adapter that, ā€œHey, this item has changed, go figure it out,ā€ because we already know theyā€™re at their position from the holder. Then, let RecyclerView decide what it wants to do.

public UserViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
  final UserViewHolder holder = UserViewHolder.create(mLayoutInflater,   
      viewGroup);
  holder.getBinding().addOnRebindCallback(new OnRebindCallback() {
    public boolean onPreBind(ViewDataBinding binding) {
      return mRecyclerView != null && mRecyclerView.isComputingLayout();
    }
    public void onCanceled(ViewDataBinding binding) {
      if (mRecyclerView == null || mRecyclerView.isComputingLayout()) {
        return;
      }
      int position = holder.getAdapterPosition();
      if (position != RecyclerView.NO_POSITION) {
        notifyItemChanged(position, DATA_INVALIDATION);
      }
    }
  });
  return holder;
}

Previously, we only had the one onBind method, so we started writing this new RecyclerView API, where you get a list of payloads. Itā€™s the list of things that change on that ViewHolder. The cool thing about this API is that you receive payloads if, and only if, RecyclerView is rebinding to the same view. You know that that view already represents the same item, but there are just some changes (maybe grammatical changes, hopefully) that you want to execute. The data invalidation payload we sent comes back to here. If itā€™s coming because of Data Binding, we just call executePendingBindings. Do you remember we didnā€™t let it update itself? Now, it is time to update itself because RecyclerView has told it to.

If youā€™re wondering what this looks like, Data Binding simply traverses the payloads, and checks to see if this data validation is the only payload it received. For example, maybe someone else is sending payloads that you donā€™t know about, which you should bail out since you donā€™t know what those changes are.

public void onBindViewHolder(UserViewHolder userViewHolder, int position) {
  userViewHolder.bindTo(mUserList.get(position));
}

public void onBindViewHolder(UserViewHolder holder, int position,
      List<Object> payloads) {
              notifyItemChanged(position, DATA_INVALIDATION);
...
}

We will ship this as a library, because it gives you performance, it gives you animations, makes everything nicer, and makes RecyclerView happy. Data Binding is mostly a happy child!

Data Invalidation is just a simple object, but I want to show it in case youā€™re curious:

static Object DATA_INVALIDATION = new Object();
private boolean isForDataBinding(List<Object> payloads) { 
  if (payloads == null || payloads.size() == 0) {
    return false;
  }
  for (Object obj : payloads) {
    if (obj != DATA_INVALIDATION) {
      return false;
    }
  }
return true;
}

Multiple View Types (28:50)

Another use case of Data Binding is multiple view types. This always happens: you have a header view, or maybe you have an application which shows search results from Google, where you can have a photo result or a place result. How can you structure this in RecyclerView? Letā€™s say you have a layout file that uses a variable, you name it ā€œdata.ā€ This name ā€œdataā€ is important because you are going to reuse the same name. You use a regular layout file:

<layout>
    <data>
        <variable name="data" type="com.example.Photo"/>
    </data>
    <ImageView android:src="@{data.url}" ā€¦/>
</layout>

If you need another type of result, for example something called ā€œplace,ā€ then you need to have a totally different layout, another XML file:

<layout>
    <data>
        <variable name="data" type="com.example.Place"/>
    </data>
    <ImageView android:src="@{data.url}" ā€¦/>
</layout>

The only thing shared between these two layout files is the variable name, which is called ā€œdata.ā€ When we do this, we create something called dataBoundViewHolder.

public class DataBoundViewHolder extends RecyclerView.ViewHolder {
  private ViewDataBinding mBinding;
  public DataBoundViewHolder(ViewDataBinding binding) {
    super(binding.getRoot());
    mBinding = binding;
  }
  public ViewDataBinding getBinding() {
    return mBinding;
  }
  public void bindTo( Place place) {
    mBinding.setPlace(place);
    mBinding.executePendingBindings();
  }
}

This is the same implementation as the previous example. It is a Real Data Binding object that keeps the binding. Real Data Binding is a base class for all generated classes. This is why you can usually keep the reference. We create this bind method ā€” previously, it was binding it to user, now itā€™s to place.

Unfortunately, thereā€™s a problem here. Thereā€™s no setPlace method in the Real Data Binding class, because itā€™s the base class. Instead, there is another API that the base class provides, which is basically a setVariable:

public void bindTo( Object obj) {
  mBinding.setVariable(BR.data, obj);
  mBinding.executePendingBindings();
}

You can provide the identifier of the variable, and then whatever object you want, like a regular Java object. The generated class is going to check the type will assign it.

The set variable looks something like this, which basically says ā€œIf the past ID is one of the IDs I know, cast it and assign it.ā€

boolean setVariable(int id, Object obj) {
  if (id == BR.data) {
    setPhoto((Photo) obj);
    return true;
  }
  return false;
}

Once you do this, the onBind, onCreate methods are exactly the same. What we do is getItemViewType, so the in the view type, we return the layout ID as the ID of the type. This works very well because when we return the layout ID and the get item leave type, RecyclerView passes it back onto the onCreateViewHolder, which will pass through the DataBindingUtil to create the correct binding class for that. Every item has its own layout, you donā€™t have to layout an object.

DataBoundViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
  return DataBoundViewHolder.create(mLayoutInflater, viewGroup, type);
}

void onBindViewHolder(DataBoundViewHolder viewHolder, int position) {
  viewHolder.bindTo(mDataList.get(position));
}

public int getItemViewType(int position) {
  Object item = mItems.get(position);
  if (item instanceof Place) {
    return R.layout.place_layout;
  } else if (item instanceof Photo) {
    return R.layout.photo_layout;
  }
  throw new RuntimeException("invalid obj");
}

Of course, if you were writing this in a production application, you would probably reserve doing instance check. You should probably have a base class that knows how to return the layout, but you get the general idea.

Binding Adapters and Callbacks (31:27)

Prepare yourselves for the coolest feature in Data Binding, according to popular pollsā€¦ (that I made). It may even be the coolest feature in all of Android. Okay, maybe Iā€™m hyping it up a little.

Letā€™s imagine you have something a little bit more complicated than setText, for example an image URL. You want to set as ImageView, and you want to set an image URL. Of course, you donā€™t want to do this on the UI thread (remember that these things are evaluated on the UI thread). You want to use Picasso or one of the other libraries out there. Maybe youā€™ll make an expression like this?

<ImageView ā€¦
    android:src="@{contact.largeImageUrl}" />

Thatā€™s not going to quite work. Where did that context come from, and what do you put it into? Thereā€™s no view. Youā€™ll lose your job if you write this. Instead what weā€™re going to do is create a BindingAdapter.

@BindingAdapter("android:src")
public static void setImageUrl(ImageView view, String url) {
    Picasso.with(view.getContext()).load(url).into(view);
}

Now the BindingAdapter here is an annotation. This oneā€™s for Android source, because weā€™re setting the attribute android:src. This is a public static method and it takes two parameters. It takes a view and a string, but note that it can also take other types too. If you wanted to have a different one that takes an int or a drawable, for example, you could do that as well. Then you fill it in, and you can put whatever you want in here. In this case, weā€™ve put in the Picasso stuff. All our code goes right in there. Now that we have the view, we can get the context. We can do whatever we want right in that code. We can now load off the UI thread, just like we want to.

Attributes Working Together (33:12)

You also might want to do something even more complex, for example in this case where we have the PlaceHolder, the source, and the image URL.

<ImageView ā€¦
   android:src="@{contact.largeImageUrl}"
   app:placeHolder="@{R.drawable.contact_placeholder}"/>

We have two different attributes, and they have two different static methods, so thatā€™s not going to work. Actually, now we can have two attributes in the same BindingAdapter. You just pass both values and fill in your Picasso code right there, right in the middle.

<ImageView ā€¦
   android:src="@{contact.largeImageUrl}"
   app:placeHolder="@{R.drawable.contact_placeholder}"/> 
@BindingAdapter(value = {"android:src", "placeHolder"},
                requireAll = false)
public static void setImageUrl(ImageView view, String url,
                               int placeHolder) {
    RequestCreator requestCreator =
        Picasso.with(view.getContext()).load(url);
    if (placeHolder != 0) {
        requestCreator.placeholder(placeHolder);
    }
    requestCreator.into(view);
}

What if you have three now? You have to have one Android source, Android PlaceHolder, and Android image URL. All these different BindingAdapters. Really, I am a little too lazy for that, so letā€™s do something else. We can have a BindingAdapter that takes all of those, or even just one or two, or any combination of them. All we have to do is set the required all to be false, and we take all those parameters. Itā€™s going to pass in all of those values. If itā€™s not provided, itā€™ll pass them in as the default value. PlaceHolder will be zero if you donā€™t have a PlaceHolder attribute in your layout. We check for that before we call the setter in the Picasso right there.

Previous Values (34:55)

You also sometimes need previous values. In this example, we have an OnLayoutChanged.

@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view,
        View.OnLayoutChangeListener oldValue,
        View.OnLayoutChangeListener newValue) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        if (oldValue != null) {
            view.removeOnLayoutChangeListener(oldValue);
        }
        if (newValue != null) {
            view.addOnLayoutChangeListener(newValue);
        }
    }
}

We want to remove the old one before we add a new one, but in this case, we donā€™t know what the old one was. We can just add the new one, and thatā€™s easy enough, but how do we remove the old one? Well, you can take that value too, weā€™ll give it to you. If you have this kind of code, we will hold on to that old value for you. So, if you have something ginormous, that is actually only transient, weā€™re going to still hold on to that. However, for cases like this, where itā€™s going to be in your memory anyway, itā€™s great. Each time it changes, to start the correct animation, you want to know what it was before.

Just using this API, Data Binding does the thinking for you. You just need to think about how you animate the change. Of course, you can also do this with multiple properties as well. Weā€™re going to pass you in. All you have to do is put all your old values first and then all your new values.

Depdendency Injection (36:20)

Letā€™s imagine that we have this adapter:

public interface TestableAdapter {
    @BindingAdapter("android:src")
    void setImageUrl(ImageView imageView, String url);
}
public interface DataBindingComponent {
    TestableAdapter getTestableAdapter();
}
DataBindingUtil.setDefaultComponent(myComponent); 
 ā€ or ā€
binding = MyLayoutBinding.inflate(layoutInflater, myComponent);

Obviously whatā€™s going to happen is when our binding code calls, itā€™s going to call this my binder setImageUrl. What if I have some state that I want to have in my BindingAdapter? Or, letā€™s say I have different kinds of BindingAdapters depending on what Iā€™m doing in my application. In that case, it gets to be a pain. What we really what we want is to have just one instance of the BindingAdapter. Where does that come from?

What we can do is create a binding component, DataBindingComponent, which is an interface. When you have an instance method, weā€™re going to generate this get my adapter into this interface. Then, itā€™s up to you to implement it. We donā€™t know how you implement it, but you implement it, and then you just set the default component.

You can also do this on a per-layout basis. In this case, one sets the default and it can be used in all of your layouts. Then we know exactly what component to use to get your adapter.

You may also want to use your component as a parameter. For example, we just saw this setImageURL before.

@BindingAdapter("android:src")
public static void setImageUrl(MyAppComponent component, 
                               ImageView view, 
                               String imageUrl) {
    component.getImageCache().loadInto(view, imageUrl);
}

We want to use some kind of state. Letā€™s imagine thatā€™s the image cache, and we want to load the image with that image cache. Where does that sum state come from? What weā€™re going to do is use the component. You can put whatever method you want in there: in this case, itā€™s [somestate].get[somestate]. Youā€™re going to pass it in as the first parameter to your BindingAdapter, and then you can do whatever you want with it. We donā€™t know anything about what youā€™re doing with your component, right? Itā€™s whatever you want to do, so it can be very convenient.

Event Handlers (38:56)

We have this onClick attribute, we have a clicked method on handler. clicked could be getClicked, or isClicked, or it could be a field, ā€œclickedā€, so how do we know what to do in this case?

<Button ā€¦
 android:onClick="@{isAdmin ? handler.adminClick : handler.userClick}" />
// No "setOnClick" method for View. Need a way to ļ¬nd it.
@BindingMethods({
    @BindingMethod(type = View.class,
                   attribute = "android:onClick",
                   method = "setOnClickListener"}) 
// Look for setOnClickListener in View 
void setOnClickListener(View.OnClickListener l) 

// Look for single abstract method in OnClickListener 
void onClick(View v);

First of all, we need to find out what onClick means? We know onClick is not setOnClick, because we looked and we saw that there was no setOnClick, but thereā€™s this binding method. It says onClick means setOnClickListener. So we look at setOnClickListener, which takes a parameter: it takes an onClickListener argument, so letā€™s look at that.

We see that thereā€™s only one abstract method in the onClickListener, so we know that this could possibly be a listener that you want to use for your event handler. Now we look at the handler, and we find a method in there, called clicked.

static class OnClickListenerImpl1 implements OnClickListener {
    public Handler mHandler;
    @Override
    public void onClick(android.view.View arg0) {
        mHandler.adminClick(arg0);
    }
}
static class OnClickListenerImpl2 implements OnClickListener {
    public Handler mHandler;
    @Override
    public void onClick(android.view.View arg0) {
        mHandler.userClick(arg0);
    }
}

We found the clicked method, and it takes the same parameters. We have a match, yay! We know this is an event handler, so we know exactly what to do: weā€™ll treat it like an event.

So what do you do in the case of TextWatcher? Cause there is no single abstract method. Thereā€™s three of them in there. In that case, what you do is you make up your own interfaces and then you merge them all together. This is essentially what I did, I merged them all together. Essentially what youā€™re doing is youā€™re merging all of the before- and on- and after changes. If theyā€™re null, then you donā€™t do anything, and if theyā€™re not null, then you do something. The required all is of course required.

Next Up: Building a Grid Layout With RecyclerView and Realm

General link arrow white

About the content

This content has been published here with the express permission of the author.

Yiğit Boyar

Yigit works on building UI widgets and improving layout performance as part of Android UI Toolkit Team. Prior to Google, he was the Android Engineering Lead at Path.com where he was mostly focused on building application architecture, real time camera filters and buttery smooth UI performance. He received his bachelorā€™s degree in computer engineering from Middle East Technical University, Turkey.

4 design patterns for a RESTless mobile integration Ā»

close