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.
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.
Receive news and updates from Realm straight to your inbox