Data binding is not yet widely used by Android developers, and those who do use it seem to limit themselves to just replacing findViewById
. However, the possibilities of data binding are endless, and with the right architecture, your code can be much cleaner and a lot easier to understand. This talk from Droidkaigi 2017 begins with explaining the basics of data binding, and then quickly moves on to more advanced techniques/functions.
What is data binding?
Data binding is a way to connect data sources with views.
It solves the problem of the messy code leftover from setting up the UI with data. By putting in a bit of extra effort, you get automatic updates, such that when the data changes, the view is updated to reflect the data.
Why use data binding?
With data binding, you can have cleaner code, simple expressions in your layout files to calculate margins, format strings, and also have powerful custom attributes.
Getting started – enable data binding
-
Enable dataBinding in your build file. Create a data binding block and set enable to true. This is done so that the data binding framework can hook into the build process.
-
Change your layout file, and add a layout tag.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns="...">
<data>
<variable name="user" type="com.databinding.User"/>
</data>
<TextView android:text="@{user.firstName}"/>
</layout>
The data tag above is a container for variables used inside the layout file. In this example, I create a variable that has the name user which can be used throughout the layout file.
The data binding framework knows the type, and you refer to it in your layout. To access this variable, use it through the regular text attribute @{
, which people tend to refer to as the ‘mustache operator.’
The mustache operator is used to describe expressions. In this case, user.firstName will make use of the firstName.
Getting started – activity setup
DataBindingUtil
is provided by the data binding framework, and in this case, it returns an instance of activity_main binding. When there’s a layout tag in the layout file, the data binding framework will generate a class for that specific file.
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil
.setContentView(this, R.layout.activity_main);
User user = new User("George");
binding.setUser(user);
}
You get an instance of that class which contains references to the views defined. It also has methods to set the variables that you define in the layout file.
How does it work?
When you start building, the data binding framework will process the layout files. It strips out the data binding specific tags, such as the layout, data, and variables tags. It parses the added expressions, and generates Java code to set all the values on the views.
You can see the generated code located in build/generate/source.
Using data binding
A way to use data binding is to replace findViewById, or ButterKnife. Use DataBindingUtil to get a reference to the binding object, then call binding.userName.setText, where userName is the TextView that has the ID userName.
Below are two examples from the actual documentation with some expressions.
// Transforming text
android:text="@{String.valueOf(index + 1)}"
// Setting views visible based on certain conditions
android:visibility="@{user.age < 18 ? View.GONE : View.VISIBLE}"
The first one sets text - it takes an integer, adds another integer to it, then makes a string out of the sum. The second is visibility based on a user age.
Dealing with updates - the options
There are two ways of dealing with updates of data in the background:
Use ObservableField
Replace all types with observable fields. As soon as a field is observable, implement a set and get method on those for the values. If you change the value of this field, it will automatically be updated in the UI.
public class User {
public final ObservableField<String>
firstName = new ObservableField<>();
}
user.firstName.set("Louise")
Extend the base class
The second option is more invasive, because you have to extend the base class. Extend BaseObservable with a field lastName where the getter should be bindable. The setter has an additional method called notifyPropertyChanged, which takes in an integer, and is an ID to a specific variable.
public class User extends BaseObservable {
private String lastName;
@Bindable
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
The name BR stands for the the binding to the R files, so the BR.lastName is a property that’s generated by the data binding framework.
Dealing with updates - under the hood
A method called notifyChange can be used to signal all of the properties to be updated. Internally, the generated code works with a long called mDirtyFlags.
private long mDirtyFlags = 0xffffffffffffffffL;
/* flag mapping
flag 0 (0x1L): user.firstName
flag 1 (0x2L): user
flag 2 (0x3L): user.lastName
flag 3 (0x4L): null
flag mapping end*/
A comment shows the actual mapping. When the data is changed, the mDirtyFlags field is updated, and this specific field is set to dirty.
public void setUser(User user) {
updateRegistration(1, user);
this.mUser = user;
synchronized(this) {
mDirtyFlags |= 0x2L;
}
notifyPropertyChanged(BR.user);
super.requestRebind();
}
It will then call notifyPropertyChanged which performs a requestRebind asynchronously. This then calls executeBindings and checks for a dirty flag to decide if it will update the value.
if ((dirtyFlags & 0xeL) != 0) {
if (user != null) {
// read user.lastName
lastNameUser = user.getLastName();
}
}
Two-way Binding for User Inputs
Suppose you have a profile screen, and you want to make it possible for people to change their location or email address - this can be accomplished with a edit text paired with two-way binding.
<EditText
android:text="@{user.firstName}"
… />
<EditText
android:text="@={user.firstName}"
… />
To indicate two-way binding, instead of writing @{
, write @={
.
When changing the text in this text field, that change will be pushed back to the ViewModel.
Custom Attributes (Binding Adapters)
Custom attributes – image loading
To start, create a public static method in the project, the framework will know this method exists because of the BindingAdapter annotation.
@BindingAdapter({"app:imageUrl", "app:error"})
public static void loadImage(ImageView view, String url, Drawable error {
Glide.with(view.getContext())
.load(url)
.error(error)
.into(view);
}
The method takes in an ImageView, and the parameters map directly to the properties in the BindingAdapter, so imageUrl maps to url, and error maps to Drawable error.
Custom attributes – font
To change the font for a specific TextView, create a BindingAdapter in a public static method that only apply to TextViews.
@BindingAdapter({"app:font"})
public static void setFont(TextView textView, String fontName) {
textView
.setTypeface(FontCache.getInstance(context)
.get(fontName));
}
<TextView
app:font="@{'alegreya'}" />
This is an example that Lisa Wray came up with a few months after data binding was released,.
Custom attributes – animations
Data binding can also be used in animations.
@BindingAdapter({"app:animatedVisibility"})
public static void setVisibility(View view, int visibility) {
…
ObjectAnimator alpha = ObjectAnimator.ofFloat(view, View.ALPHA, startAlpha, endAlpha);
…
alpha.start();
}
As an example, a custom attribute can be created. Above, animatedVisibility
takes a View along with an integer for visibility, and allows it to fade in and out.
Create the ObjectAnimator to change from one state to the other, and the animation can be started. This can be completed in a single BindingAdapter.
Check out the this blog post from the Google Developer’s Channel. There, it explains the two different ways of implementing this.
Event Handlers
Event handlers are also very important. One way to deal with event handlers with a view model is to create a standard Android event handler such as a clickListener. This can be done with the mustache operator.
With ViewModel variable in your View, add a viewModel.clickListener, and the clickListener will be triggered whenever the View is clicked.
public class UserViewModel {
…
public View.OnClickListener clickListener;
…
}
<View
android:onClick="@{viewModel.clickListener}"
… />
This also applies to TextWatchers.
textChangedListener = new TextWatcher() {
public void beforeTextChanged …
public void onTextChanged …
public void afterTextChanged …
}
<EditText
android:addTextChangedListener="@{viewModel.textChangedListener}"/>
Instead of having to implement an entire TextWatcher, implement one method for afterTextChanged, so when they are created, it’s appended to this attribute.
Tips and tricks
Data Binding with RecyclerView
When you create a RecyclerView, one the first things you do is create a ViewHolder.
public class ViewHolder extends RecyclerView.ViewHolder
{
private final ListItemBinding listItemBinding;
public ViewHolder(ListItemBinding binding) {
super(binding.getRoot());
this.listItemBinding = binding;
}
…
}
Here, create a reference to listItemBinding which was generated by the data binding framework.
Then, create a bind method on the viewHolder. In this particular bind method, I set the user, and the View knows it needs to show first and last name.
public class ViewHolder extends RecyclerView.ViewHolder
{
private final ListItemBinding listItemBinding;
…
public void bind(User user) {
listItemBinding.setUser(user);
listItemBinding.executePendingBindings();
}
}
Finally, create a generic BindingAdapter that takes in a list of objects and a layout file.
@BindingAdapter("items")
public static <T> void setItems(RecyclerView recyclerView, Collection<T> items)
{
BindingRecyclerViewAdapter<T> adapter =
(BindingRecyclerViewAdapter<T>) recyclerView.getAdapter();
adapter.setItems(items);
}
You can now instantiate it with the correct layout file along with a list of objects.
A full example of this can be seen on this GitHub project.
String formatting
String formatting can be done inside data binding expressions. Suppose there’s a string greeting, and you want to pass it a name, use the mustache operator.
<resources>
<string name="greeting">Hello, %s</string>
</resources>
<TextView
android:text="@{@string/greeting(user.firstName)}"/>
Math in expressions
With data binding, you can use math to avoid having padding/padding double/padding triple, etc.
<TextView
android:padding="@dimen/padding"
android:padding="@{@dimen/padding}"
android:padding="@{@dimen/padding * 2}"
android:padding="@{@dimen/padding + @dimen/padding}"
android:padding="@{largeScreen ? @dimen/padding * 2 : @dimen/padding}"
/>
Overriding Android attributes
You can override any default attribute that Android already provides. A BindingAdapter can be created for android:layout_margin by taking the layout parameters, changing the margin, then setting the layout parameters to the View again.
@BindingAdapter("android:layout_margin")
public static void setMargin(View view, float margin) {
MarginLayoutParams lp = (MarginLayoutParams)
view.getLayoutParams();
lp.setMargins(margin, margin, margin, margin);
view.setLayoutParams(layoutParams);
}
Resources
George Mount is a good resource for data binding. In the past eight months, he’s been blogging regularly about this topic.
A video from last year’s Google I/O with George Mount and Yigit Boyar, may be of interest. The title is Advanced Data Binding, but it also covers the basic concepts.
Receive news and updates from Realm straight to your inbox