The Android Data Binding Library has been available for a couple of years (announced at Google I/O 2015). This initial release contained quite a few powerful features, and recent updates have further enhanced its capabilities. Here, we provide the top 5 reasons Android developers should be using Android Data Binding. In other words, how data binding makes Android development easier and faster.

Note: While this is not a tutorial for data binding, it does provide examples to get developers started. Please refer to the official documentation for detailed steps for setting up and using data binding.

5 - Never use findViewById() again

With data binding, any view with an id can be referenced directly with a few simple steps.

First, wrap your layout with .

activity_sample.xml

Data binding auto-generates a Binding class for the layout. The name of the class will be the same as the layout file name converted to CamelCase with Binding appended to the end. For example, activity_sample.xml becomes ActivitySampleBinding.

Next, inflate the layout using DataBindingUtil.

In SampleActivity.java

private ActivitySampleBinding mBinding;

@Override
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 mBinding = DataBindingUtil.setContentView(
 this, R.layout.activity_sample);
 mBinding.aTextView.setText(R.string.hello);
}

Layout ids can be directly referenced with the mBinding member variable. Like the layout name, ids are converted to camelCase as fields within the Binding class. For example, android:id="@+id/a_text_view" within activity_sample.xml becomes mBinding.aTextView.

For fragments, the process is similar.

fragment_sample.xml

<?xml version="1.0" encoding="utf-8"?>

In FragmentSampleBinding.java

private ActivitySampleBinding mBinding;

@Override
public View onCreateView(LayoutInflater inflater, 
 ViewGroup container,
 Bundle savedInstanceState) {
 mBinding = FragmentSampleBinding.inflate(
 inflater, container, false);

 return mBinding.getRoot();
}

Now every id in the layout is accessible from anywhere within the activity or fragment. This also improves performance because the view hierarchy is only traversed once for the binding class, not each time findViewById() is called.

4 - Set fonts directly in layout XML (and other BindingAdapters)

This one is not as important with the announcement of Android O support for custom fonts. Until Android O is released and part of the support library, applying fonts means custom views for each TextView, Button, etc. that needs a custom font. With Data Binding, applying a font to a view is easily achieved by creating a BindingAdapter for the view.

Bindings.java

public class Bindings {
 @BindingAdapter({"font"})
 public static void setFont(TextView textView, @StringRes int resId) {
 String fontName = textView.getResources().getString(resId);
 if (!TextUtils.isEmpty(fontName)) {
 setFont(textView, fontName);
 }
 }

 @BindingAdapter({"font"})
 public static void setFont(TextView textView, String fontName) {
 FontManager.getInstance().setFont(textView, fontName);
 }
}

These two BindingAdapters allow a font to be set for a TextView using a string resource or path to the font.

fragment_sample.xml

bind:font="@{@string/font_times_new_roman}" applies the font to the TextView via the BindingAdapter. @string/font_times_new_roman is string resource containing the path to the font, such as fonts/Times-New-Roman.ttf. For reference, the FontManager class is provided below.

FontManager.java

public class FontManager {
 private static FontManager sInstance;
 private final Map mTypefaceCache;

 private FontManager() {
 mTypefaceCache = new HashMap();
 }

 public static FontManager getInstance() {
 if (sInstance == null) {
 sInstance = new FontManager();
 }

 return sInstance;
 }

 public Typeface getTypeface(Context context, int resId) {
 String customFontName = context.getString(resId);
 return getTypeface(context, customFontName);
 }

 public Typeface getTypeface(Context context, String customFontName) {
 Typeface typeface = mTypefaceCache.get(customFontName);
 if (typeface == null) {
 typeface = Typeface.createFromAsset(context.getAssets(), customFontName);
 mTypefaceCache.put(customFontName, typeface);
 }
 return typeface;
 }

 public void setFont(TextView textView, AttributeSet attrs) {
 String customFontName = getFontName(textView.getContext(), attrs);
 setFont(textView, customFontName);
 }

 public void setFont(TextView textView, String customFontName) {
 if (customFontName == null || textView == null) {
 return;
 }

 Typeface typeface = mTypefaceCache.get(customFontName);
 if (typeface == null) {
 typeface = Typeface.createFromAsset(textView.getContext().getAssets(), customFontName);
 mTypefaceCache.put(customFontName, typeface);
 }

 textView.setTypeface(typeface);
 }

 public static String getFontName(Context c, AttributeSet attrs) {
 TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.CustomFonts);
 String customFontName = typedArray.getString(R.styleable.CustomFonts_font);
 typedArray.recycle();
 return customFontName;
 }
}

Other useful BindingAdapters

Fonts are not the only thing that BindingAdapters are useful for. Here are a few other examples, and a bonus BindingConversion.

@BindingAdapter("htmlText")
public static void setText(TextView textView, String text) {
 if (!TextUtils.isEmpty(text)) {
 textView.setText(Html.fromHtml(text));
 } else {
 textView.setText(text);
 }
}

@BindingAdapter({"imageUrl"})
public static void loadImage(ImageView imageView, String url) {
 if (!TextUtils.isEmpty(url)) {
 Picasso.with(imageView.getContext()).load(url).into(imageView);
 }
}

@BindingAdapter({"number"})
public static void setTextAsNumber(TextView textView, int value) {
 textView.setText(String.format(Locale.US, "%d", value));
}

@BindingConversion
public static int convertToViewVisibility(boolean visible) {
 return visible ? View.VISIBLE : View.GONE;
}

3 - Forms (using two-way data binding)

Getting and setting values from/to EditText, CheckBox, RadioButton, etc. is tedious and can trigger infinite loops if not done with care. This is where the power of data binding really starts to shine. Data Binding enables Android developers to use a MVVM approach rather than MVC which simplifies and separates the presentation layer from the business logic. Here is an example of using this approach for a sign-in screen.

First, define the model for sign-in.

SignInModel.java

public class SignInModel extends BaseObservable {
 @Bindable
 String username;
 @Bindable
 String password;

 public String getPassword() {
 return password;
 }
 public void setPassword(String password) {
 this.password = password;
 notifyPropertyChanged(BR.password);
 }

 public String getUsername() {
 return username;
 }
 public void setUsername(String username) {
 this.username = username;
 notifyPropertyChanged(BR.username);
 }
}

Note: In order for the layout to be notified of changes to the model, the model needs to extend BaseObservable, and the fields that can change must be annotated with @Bindable.

Next, define the view model. The view model typically contains the model and any presentation-specific methods.

SignInViewModel.java

public class SignInViewModel extends BaseObservable {

 @Bindable
 private SignInModel signInModel;
 @Bindable
 private String errorUsername;

 public @NonNull SignInModel getSignInModel() {
 if (null == signInModel) {
 signInModel = new SignInModel();
 }
 return signInModel;
 }

 public String getErrorUsername() {
 return errorUsername;
 }

 public void setErrorUsername(String errorUsername) {
 this.errorUsername = errorUsername;
 notifyPropertyChanged(BR.errorUsername);
 }
}

To bind the view model to the layout, define a data element in the layout and a variable for the view model. Data can then be bound to any attribute with a getter/setter. Using @{} is for one-way binding (get values from view model and show in UI) and @={} is for two-way binding (show values and update the view model if values change in the UI). For example:

activity_sign_in.xml


...
 
...

This layout shows how to define a TextInputLayout for the username that is bound to the data.signInModel.username field of the view model. The error is bound via a BindingAdapter.

@BindingAdapter("error")
public static void setError(TextInputLayout textInputLayout, String error) {
 // This fixes a defect in TextInputLayout
 if (TextUtils.isEmpty(error)) {
 textInputLayout.setError(null);
 textInputLayout.setErrorEnabled(false);
 } else {
 textInputLayout.setError(error);
 textInputLayout.setErrorEnabled(true);
 }
}

Lastly, the view model needs to be bound to the layout.

In SignInActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);

 mViewModel = new SignInViewModel();
 mBinding = DataBindingUtil.setContentView(this, 
 R.layout.activity_sign_in);
 mBinding.setData(mViewModel);
}

2 - Callbacks (aka Listener Bindings)

How many times have you written something like this (and the corresponding listeners)?

Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(this);

Any reasonable sized Android app likely has more than 100 of these and other listeners like OnCheckChangeListener, OnItemSelectedListener, etc. Not only is this code tedious and boring to write, it tightly couples the business logic to the UI. Let us look at how this can be achieved via data binding.

First, define an interface for the callback.

SignInActionCallback.java

public interface SignInActionCallback {
 void onSignInClicked();
}

In the layout, define a variable for the callback. Then, a lambda expression can be used to bind the onClickListener to the callback method.

activity_sign_in.xml


...
 
...

Lastly, implement the callback interface and bind the callback to the layout.

In SignInActivity.java

private SignInActionCallback mCallback = new SignInActionCallback() {
 @Override
 public void onSignInClicked() {
 attemptLogin();
 }
};

@Override
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);

 mViewModel = new SignInViewModel();
 mBinding = DataBindingUtil.setContentView(this, 
 R.layout.activity_sign_in);
 mBinding.setData(mViewModel);
 mBinding.setCallback(mCallback);
}

Note: The callback implementation must be its own class (anonymous inner class is ok). Do not let activity/fragment implement it since auto-generated code will fail.

For more information and examples, see the Data Binding documentation on Listener Bindings and passing parameters.

While this looks like a fair bit of code for a single callback, adding more callbacks in the same activity/fragment is simply adding a method to the interface, and adding the listener binding to the layout. In the next section, we will show you how this can be made highly extensible.

1 - The last adapter you will ever need!

One of the most boring, repetitive, tedious tasks in Android is creating an adapter for every list, recycler view, or pager in the app, especially since nearly every adapter does the same thing except with different view holders. If #2-#5 has not convinced you that using data binding will make your life easier, this one will. This is the last adapter you will ever need.

Note: Thank you to Android Framework Team for providing great examples at https://github.com/google/android-ui-toolkit-demos upon which this is built.

Building upon the view model and listener bindings from #2 and #3, let's start with the pattern for items within the adapter. Each item has a view model defined by a Java object and a corresponding view (layout). Each item extends the following:

BaseObservableWithLayoutItem.java

abstract public class BaseObservableWithLayoutItem extends BaseObservable implements LayoutBinding {
}

LayoutBinding.java

public interface LayoutBinding {
 /**
 * Get the layout resource ID for an view that needs to be bound.
 * @return the resource ID of the layout
 */
 @LayoutRes
 int getLayoutId();
}

For example, a sample item would look like:

SamplePostItem.java

@Parcel
public class SamplePostItem extends BaseObservableWithLayoutItem {

 @Bindable
 SamplePostModel post;

 SamplePostItem() {}

 public SamplePostItem(SamplePostModel postModel) {
 this.post = postModel;
 }

 @Override
 public int getLayoutId() {
 return R.layout.item_sample_post;
 }

 public SamplePostModel getPost() {
 return post;
 }

}

Note: This example is using the Parceler library for easily converting POJOs to Parcelables.

SamplePostModel.java

@Parcel
public class SamplePostModel extends BaseObservable {

 String title;
 String body;

 // Protected constructor for Parcel
 SamplePostModel() {}

 public String getTitle() {
 return title;
 }

 public String getBody() {
 return body;
 }
}

item_sample_post.xml

<?xml version="1.0" encoding="utf-8"?>

You may have noticed that the layout has a callback defined. We will get to that in a moment. First, let's look at the activity's view model.

MainViewModel.java

@Parcel
public class MainViewModel extends BaseObservable {

 @Bindable
 @Transient
 private MultiTypeDataBoundAdapter dataBoundAdapter;

 List postItems;

 // Protected constructor for Parcel
 MainViewModel() {
 postItems = new ArrayList<>();
 }

 public MainViewModel(MainActionCallback actionCallback) {
 this();
 setActionCallback(actionCallback);
 }

 public void setActionCallback(MainActionCallback actionCallback) {
 setDataBoundAdapter(new MultiTypeDataBoundAdapter(actionCallback));
 update();
 }

 public MultiTypeDataBoundAdapter getDataBoundAdapter() {
 return dataBoundAdapter;
 }

 public void setDataBoundAdapter(MultiTypeDataBoundAdapter dataBoundAdapter) {
 this.dataBoundAdapter = dataBoundAdapter;
 notifyPropertyChanged(BR.dataBoundAdapter);
 }

 public void clear() {
 postItems.clear();
 if (null != dataBoundAdapter) {
 dataBoundAdapter.clear();
 }
 }

 public void updatePosts(@Nullable List postModels) {
 postItems.clear();
 if (null != postModels
 && !postModels.isEmpty()) {
 for (SamplePostModel model : postModels) {
 if (null != model) {
 postItems.add(new SamplePostItem(model));
 }
 }
 }
 update();
 }

 public void update() {
 if (null != dataBoundAdapter) {
 dataBoundAdapter.clear();
 dataBoundAdapter.addItems(postItems.toArray());
 }
 }

}

The main activity's view model contains two fields (an adapter and a list of items) and a bit of code for refreshing the adapter if the callback or the data change. The activity layout just contains a recycler view.

activity_main.xml

One of the great features about data binding is that it automatically looks for getters and setters for layout attributes, so we did not need to define a BindingAdapter to bind the adapter to the recycler view. That makes the activity binding code no different than any other view model. Here is what the activity looks like.

MainActivity.java

public class MainActivity extends BaseCoreActivity {

 private static final String KEY_STATE_MAIN_VIEW_MODEL = "KEY_STATE_MAIN_VIEW_MODEL";

 private ActivityMainBinding mBinding;

 private MainViewModel mViewModel;

 private MainActionCallback mCallback = new MainActionCallback() {
 @Override
 public void onItemClicked(SamplePostItem item) {
 if (null != item
 && null != item.getPost())
 Toast.makeText(MainActivity.this, item.getPost().getTitle() + " clicked!", Toast.LENGTH_SHORT)
 .show();
 }
 };

 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 if (null != savedInstanceState) {
 if (savedInstanceState.containsKey(KEY_STATE_MAIN_VIEW_MODEL)) {
 mViewModel = Parcels.unwrap(savedInstanceState.getParcelable(KEY_STATE_MAIN_VIEW_MODEL));
 }
 }
 if (null == mViewModel) {
 mViewModel = new MainViewModel(mCallback);
 } else {
 mViewModel.setActionCallback(mCallback);
 }
 mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
 mBinding.setData(mViewModel);
 mBinding.setCallback(mCallback);

 mBinding.executePendingBindings();

 setupActionBar();

 requestPosts();
 }

 @Override
 public void onSaveInstanceState(Bundle outState) {
 super.onSaveInstanceState(outState);
 outState.putParcelable(KEY_STATE_MAIN_VIEW_MODEL, Parcels.wrap(mViewModel));
 }

 private void setupActionBar() {
 ActionBar actionBar = getSupportActionBar();
 if (null != actionBar) {
 actionBar.setDisplayHomeAsUpEnabled(false);
 }
 }

 @Override
 public void handleDataResponse(DataResponse dataResponse) {
 super.handleDataResponse(dataResponse);
 if (dataResponse instanceof SamplePostsDataResponse) {
 handleSamplePostsDataResponse((SamplePostsDataResponse) dataResponse);
 }
 }

 private void requestPosts() {
 sendRequest(new SamplePostsDataRequest.Builder(this).build());
 }

 private void handleSamplePostsDataResponse(@NonNull SamplePostsDataResponse response) {
 if (response.isSuccess()) {
 mViewModel.updatePosts(response.getPosts());
 }
 }
}

There really isn't anything special about the activity. In fact, every activity/fragment that uses this approach to data binding will have nearly identical code with a different view model and callback. You may have noticed that it even makes saving state simple. Note that there is a bit of code in the activity for sending and receiving data. You can use any method that is needed to retrieve data and update it in the view model. Now, let's look at that callback.

MainActionCallback.java

public interface MainActionCallback extends MultiTypeDataBoundAdapter.ActionCallback {
 void onItemClicked(SamplePostItem item);
}

It looks very similar to the SignInActionCallback above except it extends a MultiTypeDataBoundAdapter.ActionCallback, an empty interface defined by the magical (ok, not magical, but still pretty cool) MultiTypeDataBoundAdapter. By extending a common interface, every recycler view can use the same adapter to bind the view model and callback to any item in the adapter. To add a different item to the recycler view, simply create a new item view model that extends BaseObservableWithLayoutItem and a corresponding layout and then it can be added to the adapter. If the item needs a callback, add a method to the existing MainActionCallback. Each item in the adapter is updated without needing to find the item within the adapter - get the view holder, update the data, and notify the UI to update. This also makes every item an independent, testable view model that encapsulates its own logic. The activity/fragment becomes only a router of user interactions.

If you made it this far, congratulations! Here's the adapter.

MultiTypeDataBoundAdapter.java

public class MultiTypeDataBoundAdapter extends BaseDataBoundAdapter {
 private List mItems = new ArrayList<>();
 private ActionCallback mActionCallback;

 public MultiTypeDataBoundAdapter(ActionCallback actionCallback, Object... items) {
 mActionCallback = actionCallback;
 if (null != items) {
 Collections.addAll(mItems, items);
 }
 }

 @Override
 protected void bindItem(DataBoundViewHolder holder, int position, List payloads) {
 Object item = mItems.get(position);
 holder.binding.setVariable(BR.data, mItems.get(position));
 // this will work even if the layout does not have a callback parameter
 holder.binding.setVariable(BR.callback, mActionCallback);
 if (item instanceof DynamicBinding) {
 ((DynamicBinding) item).bind(holder);
 }
 }

 @Override
 public @LayoutRes
 int getItemLayoutId(int position) {
 // use layout ids as types
 Object item = getItem(position);

 if (item instanceof LayoutBinding) {
 return ((LayoutBinding) item).getLayoutId();
 }
 if (BuildConfig.DEBUG) {
 throw new IllegalArgumentException("unknown item type " + item);
 }
 return -1;
 }

 @Override
 public int getItemCount() {
 return mItems.size();
 }

 protected final List getItems() {
 return mItems;
 }

 @Nullable
 public final Object getItem(int position) {
 return position < mItems.size() ? mItems.get(position) : null;
 }

 public final int indexOf(Object item) {
 return mItems.indexOf(item);
 }

 public final void addItem(Object item) {
 mItems.add(item);
 notifyItemInserted(mItems.size() - 1);
 }

 public final void addItem(int position, Object item) {
 mItems.add(position, item);
 notifyItemInserted(position);
 }

 public final void addItems(Object... items) {
 if (null != items) {
 int start = mItems.size();
 Collections.addAll(mItems, items);
 notifyItemRangeChanged(start, items.length);
 }
 }

 public final void removeItem(Object item) {
 int position = mItems.indexOf(item);
 if (position >= 0) {
 mItems.remove(position);
 notifyItemRemoved(position);
 }
 }

 public final void removeItems(Object... items) {
 if (null != items) {
 int size = mItems.size();
 mItems.removeAll(Arrays.asList(items));
 notifyItemRangeChanged(0, size);
 }
 }

 public void clear() {
 int size = mItems.size();
 mItems.clear();
 notifyItemRangeRemoved(0, size);
 }

 /**
 * Class that all action callbacks must extend for the adapter callback.
 */
 public interface ActionCallback {}

}

MultiTypeDataBoundAdapter extends a BaseDataBoundAdapter which does the heavy lifting for the RecyclerView.

MainActionCallback.java

abstract public class BaseDataBoundAdapter
 extends RecyclerView.Adapter> {
 private static final Object DB_PAYLOAD = new Object();
 @Nullable
 private RecyclerView mRecyclerView;

 /**
 * This is used to block items from updating themselves. RecyclerView wants to know when an
 * item is invalidated and it prefers to refresh it via onRebind. It also helps with performance
 * since data binding will not update views that are not changed.
 */
 private final OnRebindCallback mOnRebindCallback = new OnRebindCallback() {
 @Override
 public boolean onPreBind(ViewDataBinding binding) {
 if (mRecyclerView == null || mRecyclerView.isComputingLayout()) {
 return true;
 }
 int childAdapterPosition = mRecyclerView.getChildAdapterPosition(binding.getRoot());
 if (childAdapterPosition == RecyclerView.NO_POSITION) {
 return true;
 }
 notifyItemChanged(childAdapterPosition, DB_PAYLOAD);
 return false;
 }
 };

 @Override
 @CallSuper
 public DataBoundViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
 DataBoundViewHolder vh = DataBoundViewHolder.create(parent, viewType);
 vh.binding.addOnRebindCallback(mOnRebindCallback);
 return vh;
 }

 @Override
 public final void onBindViewHolder(DataBoundViewHolder holder, int position,
 List payloads) {
 // when a VH is rebound to the same item, we don't have to call the setters
 if (payloads.isEmpty() || hasNonDataBindingInvalidate(payloads)) {
 bindItem(holder, position, payloads);
 }
 holder.binding.executePendingBindings();
 }

 /**
 * Override this method to handle binding your items into views
 *
 * @param holder The ViewHolder that has the binding instance
 * @param position The position of the item in the adapter
 * @param payloads The payloads that were passed into the onBind method
 */
 protected abstract void bindItem(DataBoundViewHolder holder, int position,
 List payloads);

 private boolean hasNonDataBindingInvalidate(List payloads) {
 for (Object payload : payloads) {
 if (payload != DB_PAYLOAD) {
 return true;
 }
 }
 return false;
 }

 @Override
 public final void onBindViewHolder(DataBoundViewHolder holder, int position) {
 throw new IllegalArgumentException("just overridden to make final.");
 }

 @Override
 public void onAttachedToRecyclerView(RecyclerView recyclerView) {
 mRecyclerView = recyclerView;
 }

 @Override
 public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
 mRecyclerView = null;
 }

 @Override
 public final int getItemViewType(int position) {
 return getItemLayoutId(position);
 }

 @LayoutRes
 abstract public int getItemLayoutId(int position);
}

Finally, for items that need to do a bit of work when the item is bound to the recycler view, the item view model can implement this interface. The adapter will call the bind(… ) method when the item it is bound. This is especially useful for items that contain recycler views themselves and need to populate the data in their own adapter.

DynamicBinding.java

public interface DynamicBinding {
 /**
 * Gives the item an opportunity to do work during binding.
 */
 void bind(DataBoundViewHolder holder);
}

Conclusion

We hope this article provides some good reasons and examples for using Android Data Binding. Using even one of these examples will simplify and speed up development. There is much more to Android Data Binding beyond what is provided here, and we encourage developers to refer to the official documentation for more detailed explanations.

Bonus: We have worked so hard to do everything with data binding. Here is a way to add item decorations too!

@BindingAdapter({"dividerItemDecoration"})
public static void setDividerItemDecoration(RecyclerView recyclerView, Drawable dividerDrawable) {
 if (null != dividerDrawable) {
 RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
 if (layoutManager instanceof LinearLayoutManager) {
 DividerItemDecoration itemDecoration = new DividerItemDecoration(recyclerView.getContext(), ((LinearLayoutManager) layoutManager).getOrientation());
 itemDecoration.setDrawable(dividerDrawable);
 recyclerView.addItemDecoration(itemDecoration);
 }
 }
}