very time you look at those numerous lines of code with findViewById and ternary operations with visibility you want Cthulhu to finally revel across the Earth so you don’t have to see it anymore, don’t you? But trust me, there’s a way. And we will show it to you under cut.
Plan
- What kind of beast is it
- Integration
- First try
- The fun has just begun
- Coding with pleasure
- And that’s not all!
- Takeaways
- Nothing is perfect
- Conclusion
1. What kind of beast is it
We all know that the most boring part of developer’s work is describing UI behavior depending on data change logic. Numerous hours were wasted on writing and maintaining trivial and almost useless code occupying tens or even hundreds of lines in almost any activity or fragment. Not to mention readability issues caused by them: to wade through the walls of code or find bugs in it you’ve got to be a tireless adventurer.
So the message is clear: we need a way to automate this process. DataBinding library is doing exactly that — it helps to noticeably minimize the code for app logic binding it with its view.
2. Integration
To show you the potential of this solution we will write a small app with user profile. First, let’s connect this wonderful tool to our project.
To integrate binding we need to use Gradle 1.5.0 or above. Let’s update build.gradle file of the project by adding the following string:
buildscript {
...
dependencies {
classpath 'com.android.tools.build:gradle:1.5.0'
}
}
Now add dataBinding into the modular build.gradle:
android {
...
dataBinding {
enabled = true
}
}
Synchronize Gradle — now DataBinding is available in our project :)
3. First try
Time to proceed to our sample app. We are going to start small: create an activity displaying main user info (name, surname, status and online indicator).
It’s a simple task so let’s do it! Create User model with the necessary fields:
public class User {
/* constructor */
private String name;
private String surname;
private String status;
private boolean isOnline;
/* getters and setters */
}
Now, let’s create a layout. We will only use simple TextView for displaying name and status and View for indicator:
<layout
xmlns:android="http://schemas.android.com/apk/res/android" >
<data>
<variable
name="user"
type="com.example.models.User" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:layout_width="@dimen/indicator_size"
android:layout_height="@dimen/indicator_size"
android:background="@{user.isOnline ? @drawable/shape_online : @drawable/shape_offline}"
.../>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name + ' ' + user.surname}"
.../>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.status}"
.../>
</RelativeLayout>
</layout>
Here’s an explanation for those strange tags in the layout:
-
<layout>
— we place the root element between this tag to tell the compiler that layout file pertains to the binding. It is worth noting that you should always put it in the root. -
<data>
is always put in the layout and serves as as wrapper for variables used in the layout. -
<variable>
contains name and type describing the name of the variable and its full name respectively (including the package name). -
@{}
container used for describing the expression. For example, putting name and surname in one string or simple field display. We will come back to expressions later.
Now let’s see how our activity class will look like:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, LAYOUT);
binding.setUser(Demo.getUser());
}
Here we are using DataBindingUtil to initialize layout and receive ActivityMainBinding that was generated for us in reply (by default, Binding class is generated based on the layout file name converted into CamelCase with “Binding” suffix added to it). ActivityMainBinding is used for saving links to the elements of our interface. To fill them with data you need to specify user object with setUser method (name of this object depends on the name of the variable in the name field of variable block).
Let’s take a look at the result:
Yepp, this code is enough for showing the data you need :) But...
4. The fun has just begun
DataBinding has its own expression language for layout files. It corresponds to Java expressions and has quite impressive capabilities. Below you can see the list of all available operators:
- mathematical operators;
- string concatenation;
- logical operators;
- binary operators;
- unary operators;
- bit shifting;
- comparison operators;
- instanceof;
- grouping;
- literals: string, numerical, symbolic, null;
- type casting;
- method calls and access to fields;
- access to array elements and List;
- “?:” ternary operator.
But to prevent the layout file from turning into a collection of app logic, several operators are not supported:
- this;
- super;
- new;
- explicit execution of typed methods.
Null Coalescing Operator “??” that allows you to perform most null checks is also worth mentioning. Here’s how it looks like:
android:text="@{user.status ?? user.lastSeen}"
android:text="@{user.status != null ? user.status : user.lastSeen}"
Both lines of code do the same job. Isn’t that great? :)
Code generated by DataBinding library also automatically checks all objects and prevents NullPointerException. For example, if in expression @{user.status} status field equals null, its value would be equal to the default one, i. e. “null”. This principle works for primitive data types as well.
<data>
tag we already know has another property — you can use it to import types you need in your work. Furthermore, you can shorten them for convenience and saving space using alias.
<data>
<import type="android.view.View" alias="v"/>
</data>
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isNinja ? v.GONE : v.VISIBLE}"/>
Work with resources definitely deserves a mention: almost all the resources can be called directly and combined with logical operators. Awesome! Example with the online indicator demonstrates one of the possible implementations: inserting the required Drawable into android:background attribute depending on whether the user is online or not. And now just imagine the flexibility of the layout using all the possible links:
Type | Normal link | Link in the expression |
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
Color int | @color | @color |
ColorStateList | @color | @colorStateList |
Apart from that, @BindingAdapter also gets some attention. You can use it to redefine the behavior of the existing attributes and create your own attributes without thinking about attrs.xml.
In order to do it you need to create a public static method accepting as input View of the necessary type and value we specify in the layout. The method itself should have @BindingAdapter annotation and in its body you should specify string with the name of the attribute.
For example, here’s an adapter that allows to attach any method without parameters as a OnClickListener to the view:
@BindingAdapter("app:onClick")
public static void bindOnClick(View view, final Runnable runnable) {
view.setOnClickListener(v -> runnable.run());
}
Adapters can be created for several parameters simultaneously. For example, here’s how you can specify url for image or resource in case of download error:
@BindingAdapter({"android:src", "app:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.with(view.getContext())
.load(url)
.error(error)
.into(view);
}
But sometimes you need to transform types automatically, for example boolean into int (Visibility)). In such cases replace adapter with @BindingConversion:
@BindingConversion
public static int convertBooleanToVisibility(boolean visible) {
return visible ? View.VISIBLE : View.GONE;
}
Unlike adapter that takes full control over the behavior, @BindingConversion has a return type that will be later applied to the attribute.
5. Coding with pleasure
Let’s improve our app with additional functions. First of all, our profile lacks personal touch since it has no photo. Secondly, it is almost useless since there are no actions.
So let’s add user photo and “Add friend” option. Add the necessary fields to the User model:
private String photo;
private boolean isFriend;
Add ImageView for user avatar into the layout:
<ImageView
android:layout_width="@dimen/avatar_size"
android:layout_height="@dimen/avatar_size"
android:src="@{user.photo}"/>
loadImage adapter we have written earlier will be used for uploading image. Now, let’s introduce buttons for adding and deleting friends:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/add_as_friend"
android:visibility="@{!viewModel.isFriend}"
app:onClick="@{viewModel.changeFriendshipStatus}"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/already_friends"
android:visibility="@{viewModel.isFriend}"
app:onClick="@{viewModel.changeFriendshipStatus}"/>
</RelativeLayout>
You have probably noticed that user object was renamed into viewModel even though it’s an entirely different object. ViewModel interlinks View with data model describing all the behavior logic. We will talk about MVVM (Model-View-ViewModel) next time and for now you’ve got to remember that you need to put your behavior logic into a separate object. Here’s our VM for a profile:
public class ProfileViewModel extends BaseObservable {
public static final int LOADING_SHORT = 1000;
private boolean isLoaded;
private boolean isFriend;
public ProfileViewModel(boolean isFriend) {
this.isFriend = isFriend;
this.isLoaded = true;
}
@Bindable
public boolean getIsLoaded() {
return this.isLoaded;
}
public void setIsLoaded(boolean isLoaded) {
this.isLoaded = isLoaded;
notifyPropertyChanged(BR.isLoaded);
}
@Bindable
public boolean getIsFriend() {
return this.isFriend;
}
public void setIsFriend(boolean isFriend) {
this.isFriend = isFriend;
notifyPropertyChanged(BR.isFriend);
}
public void changeFriendshipStatus() {
load(() -> setIsFriend(!isFriend));
}
private void load(Runnable onLoaded) {
setIsLoaded(false);
new Handler().postDelayed(() -> {
setIsFriend(!isFriend);
setIsLoaded(true);
}, LOADING_SHORT);
}
}
As you can see, VM is inherited from BaseObservable. It allows us to notify binding about internal changes using notifyPropertyChanged where BR.variable is transferred. Name of the variable is generated in the same way as id in a standard R class and is marked with @Bindable annotation (in getters in our case).
isLoaded field will serve as a flag for a loading indicator that will switch for a second each time changeFriendshipStatus is called. Now we need to slightly modify the layout with buttons and add a ProgressBar:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="@{viewModel.isLoaded}">
<!-- buttons -->
</RelativeLayout>
<ProgressBar
android:layout_width="@dimen/small_progressbar_size"
android:layout_height="@dimen/small_progressbar_size"
android:layout_gravity="center"
android:visibility="@{!viewModel.isLoaded}"/>
Now each time we press button for adding or deleting friends RelativeLayout will be replaced with ProgressBar for a second and return to its normal state. Time to run the code and check the result.
6. And that’s not all!
Have you noticed those bulky @Bindable and notifyPropertyChanged getters and setters in ViewModel? They clutter up the code and force us to do a lot of routine work (and that’s exactly what we wanted to avoid with binding). But everything is not lost — we have ObservableField<T>.
ObservableField are autonomous observable objects with one field. You can access them with get() and set() methods that automatically notify View about changes. To use it you need to create a public final field in VM class. Let’s update our ProfileViewModel to do it:
public class ProfileViewModel {
public static final int LOADING_SHORT = 1000;
public final ObservableBoolean isLoaded = new ObservableBoolean(true);
public final ObservableBoolean isFriend = new ObservableBoolean();
public ProfileViewModel(boolean isFriend) {
isFriend.set(isFriend);
}
public void changeFriendshipStatus() {
load(() -> isFriend.set(!isFriend.get()));
}
private void load(Runnable onLoaded) {
isLoaded.set(false);
new Handler().postDelayed(() -> {
isFriend.set(!isFriend.get());
isLoaded.set(true);
}, LOADING_SHORT);
}
}
Now we obviously have less code even though it executes the same functions. It is worth noting that almost every primitive type has an observable analogue. We used ObservableBoolean.
Unfortunately the library doesn’t have two-way data binding yet. But we have all the tools to implement it ourselves! Two-way data binding means that the data influences what we see in View and data entered by the user changes the model as well. Lets implement it for EditText.
First off, note that ObservableString is absent for some reason so we need to create it:
public class ObservableString extends BaseObservable {
private String value = "";
public ObservableString(String value) {
this.value = value;
}
public ObservableString() { }
public String get() {
return value != null ? value : "";
}
public void set(String value) {
if (value == null) value = "";
if (!this.value.contentEquals(value)) {
this.value = value;
notifyChange();
}
}
public boolean isEmpty() {
return value == null || value.isEmpty();
}
public void clear() { set(null); }
}
Logic is simple here so we won’t discuss it and will proceed to adapter implementation. The main trick is to add TextWatcher to EditText and update our ObservableString in the callback:
@BindingAdapter("android:text")
public static void bindEditText(EditText view,
final ObservableString observableString) {
Pair<ObservableString, TextChangeListener> pair = (Pair) view.getTag(R.id.bound_observable);
if (pair == null || pair.first != observableString) {
if (pair != null) view.removeTextChangedListener(pair.second);
TextChangeListener watcher = new TextChangeListener(
(s, start, before, count) -> observableString.set(s.toString()));
view.setTag(R.id.bound_observable, new Pair<>(observableString, watcher));
view.addTextChangedListener(watcher);
}
String newValue = observableString.get();
if (!view.getText().toString().equals(newValue))
view.setText(newValue);
}
Now we can simply specify our ObservableString in XML and that will be all!
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.status}"
style="@style/StatusTextView"/>
7. Takeaways
Our team has been using this library for quite a while and we have created several solutions that make our life easier. Here are some of them:
RecyclerBindingAdapter is a universal adapter for simple lists. Single one for the whole project. Now we don’t have to create a separate one for each list :) Let’s see how it works:
public class RecyclerBindingAdapter<T>
extends RecyclerView.Adapter<RecyclerBindingAdapter.BindingHolder> {
private int holderLayout, variableId;
private AbstractList<T> items = new ArrayList<>();
private OnItemClickListener<T> onItemClickListener;
public RecyclerBindingAdapter(int holderLayout, int variableId, AbstractList<T> items) {
this.holderLayout = holderLayout;
this.variableId = variableId;
this.items = items;
}
@Override
public RecyclerBindingAdapter.BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext())
.inflate(holderLayout, parent, false);
return new BindingHolder(v);
}
@Override
public void onBindViewHolder(RecyclerBindingAdapter.BindingHolder holder, int position) {
final T item = items.get(position);
holder.getBinding().getRoot().setOnClickListener(v -> {
if (onItemClickListener != null)
onItemClickListener.onItemClick(position, item);
});
holder.getBinding().setVariable(variableId, item);
}
@Override
public int getItemCount() {
return items.size();
}
public void setOnItemClickListener(OnItemClickListener<T> onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
public interface OnItemClickListener<T> {
void onItemClick(int position, T item);
}
public static class BindingHolder extends RecyclerView.ViewHolder {
private ViewDataBinding binding;
public BindingHolder(View v) {
super(v);
binding = DataBindingUtil.bind(v);
}
public ViewDataBinding getBinding() {
return binding;
}
}
You can create it with a single line:
new RecyclerBindingAdapter<>(R.layout.item_holder, BR.data, list);
BR.data — is a variable name in xml file and list is our sample. Specify RecyclerView and eliminate this pain in the neck once and for all. Getting slightly ahead here’s the question: how can we configure RecyclerView without directly addressing the binding? And here where we need the next feature — RecyclerConfiguration:
public class RecyclerConfiguration extends BaseObservable {
private RecyclerView.LayoutManager layoutManager;
private RecyclerView.ItemAnimator itemAnimator;
private RecyclerView.Adapter adapter;
/* @Bindable getters */
/* notifyPropertyChanged setters */
@BindingAdapter("app:configuration")
public static void configureRecyclerView(RecyclerView recyclerView, RecyclerConfiguration configuration) {
recyclerView.setLayoutManager(configuration.getLayoutManager());
recyclerView.setItemAnimator(configuration.getItemAnimator());
recyclerView.setAdapter(configuration.getAdapter());
}
}
This simple wrapper doesn’t clutter up ObservableFileds code and allows you to specify configuration right in XML (but you need to fill it up from the code in advance).
Another interesting practice is <include>
tag. In binding it has totally different function, namely it is used for replacing simple CustomView. Let’s add photo counter, friends and likes to our project. Layout element will consist of title and the counter itself. To avoid code duplication we will put it in item_counter.xml:
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="count"
type="int"/>
<variable
name="title"
type="String"/>
</data>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{Integer.toString(count)}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{title}"/>
</LinearLayout>
</layout>
For import to work you need to wrap the layout with <layout>
tag. By adding <data>
you can turn all the values into variables. The most exciting thing is that you can specify this variables from the outside using attributes with the same name! Here’s how to transfer data to the friend counter:
<include
layout="@layout/item_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:title="@{@plurals/friends(viewModel.friendsCount)}"
app:count="@{viewModel.friendsCount}"/>
And after making some additions to ViewModel we will get the following imitation of realtime events:
8. Nothing is perfect
As of today DataBinding is still on the development stage that’s why it has a couple of drawbacks. First of all, it doesn’t have two-way data binding and some of the fields available out of the box. There’re also problems with Android Studio: often expressions from the layout are marked as errors, BR class or the whole binding package disappears (to solve it use Build → Clean Project). There problems with encoding in the layout (for example instead of “&&” you need to write “&&”) etc. But those are simply temporary inconveniences since the library is actively developed and it’s worth waiting for it to improve :)
9. Conclusion
Those are not all the advantages of DataBiding library but it’s time to end this article. I hope we have sparked your interest in this library, you have learned something new and will put this knowledge into practice.
Next time we are going to discuss the best way to implement MVVM app architecture that is closely connected with DataBiding.
Thanks for reading!
Sample app is available on GitHub.
Need MVP development, iOS and Android apps or prototyping? Check out our portfolioand make an order today!