In Part 1 I gave an introduction into the Android Data Binding Library. I suggest reading that post first before reading further.
In this post I am going to discuss the following :
- Binding Adapters
- Binding Events
- Two Way Binding
Binding Adapters
Creating an adapter and using Android Data Binding for each item is quite simple. Below is an example of how your RecyclerView adapter will look after using Data Binding:
public class BookAdapter extends RecyclerView.Adapter<BookAdapter.ViewHolder> { private final Context context; private List<BookDetail> bookDetails; public static class ViewHolder extends RecyclerView.ViewHolder { private ViewDataBinding viewDataBinding; public ViewHolder(View v) { super(v); viewDataBinding = DataBindingUtil.bind(v); v.setTag(viewDataBinding); } public ViewDataBinding getBinding(){ return viewDataBinding; } } private View.OnClickListener onClickListener; public BookAdapter(List<BookDetail> bookDetails, Context context, View.OnClickListener onClickListener) { this.bookDetails = bookDetails; this.context = context; this.onClickListener = onClickListener; } @Override public BookAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()) .inflate(R.layout.list_item_book, parent, false); return new ViewHolder(v); } @Override public void onBindViewHolder(ViewHolder holder, int position) { BookDetail bookDetail = bookDetails.get(position); holder.getBinding().setVariable(BR.book, bookDetail); holder.getBinding().setVariable(BR.itemNumber, position); holder.getBinding().setVariable(BR.click_listener, onClickListener); holder.getBinding().executePendingBindings(); } public BookDetail getBook(int position){ if (bookDetails == null){ return null; } return bookDetails.get(position); } @Override public int getItemCount() { return bookDetails.size(); } }
- The first difference in this method is that our ViewHolder object now only contains a ViewDataBinding object (instead of ImageViews and TextViews) and a method to get the binding object.
- The method onBindViewHolder binds the view to the item variables. This is shown by the lines holder.getBinding().setVariable(BR.book, bookDetail) . This sets the views book variable defined in the layout XML.
- The method holder.getBinding().executePendingBindings() triggers the View to be updated with the new values provided. This method has to be run on the UI thread.
- Within our list_item_book.xml layout file we define and access variables in the same way as in Part 1. With the <data> section defining all the variables to use in the xml, including the imports. The Views then contain reference to those objects. For example android:text=”@{book.bookTitle}” accesses the books title and assigns the value to the text of the TextView.
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <import type="android.view.View"/> <import type="android.text.TextUtils"/> <variable name="book" type="com.examplecompany.pojo.BookDetail"/> <variable name="click_listener" type="View.OnClickListener"/> <variable name="itemNumber" type="Integer"/> </data> <LinearLayout xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="4dp" android:paddingBottom="8dp"> <android.support.v7.widget.CardView android:id="@+id/card_view" android:layout_width="@dimen/book_column_width" android:layout_height="wrap_content" android:layout_gravity="center" android:foreground="@drawable/book_selector" android:onClick="@{click_listener}" app:specialTag="@{book}" android:paddingBottom="8dp" app:cardCornerRadius="2dp" > <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/imageViewBookCover" android:layout_width="@dimen/book_column_width" android:layout_height="@dimen/book_column_width" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" app:imageUrlWeb="@{book.bookCoverUrl}" android:layout_centerVertical="true" android:layout_gravity="center_horizontal"/> <TextView android:id="@+id/textViewBookName" style="@style/TextAppearance.AppCompat.Title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:text="@{book.bookTitle}" android:layout_below="@+id/imageViewBookCover" android:layout_gravity="center_horizontal" android:layout_marginBottom="4dp" android:layout_marginTop="4dp" android:paddingLeft="4dp" android:paddingRight="4dp" tools:text="Giraffe"/> </RelativeLayout> </android.support.v7.widget.CardView> </LinearLayout> </layout>
Events
In the same way that we bind variables in XML, we can set variables for click listeners in XML.
- Create the click listener in code:
private View.OnClickListener bookClickListener = new View.OnClickListener() { @Override public void onClick(View v) { final BookDetail bookDetail = (BookDetail) v.getTag(); ..... } };
2. In list_item_book.xml, define the View.OnClickListener and set the View that the click should fire on by using the variable defined android:onClick=”@{click_listener}” . This will then trigger the click event.
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <import type="android.view.View"/> <import type="android.text.TextUtils"/> <variable name="book" type="com.examplecompany.pojo.BookDetail"/> <variable name="click_listener" type="View.OnClickListener"/> </data> <LinearLayout xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="4dp" android:paddingBottom="8dp"> <!-- A CardView that contains a TextView --> <android.support.v7.widget.CardView android:id="@+id/card_view" android:layout_width="@dimen/book_column_width" android:layout_height="wrap_content" android:layout_gravity="center" android:foreground="@drawable/book_selector" android:onClick="@{click_listener}" app:specialTag="@{book}" android:paddingBottom="8dp" app:cardCornerRadius="2dp" > <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/imageViewBookCover" android:layout_width="@dimen/book_column_width" android:layout_height="@dimen/book_column_width" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" app:imageUrlWeb="@{book.bookCoverUrl}" android:layout_centerVertical="true" android:layout_gravity="center_horizontal"/> <TextView android:id="@+id/textViewBookName" style="@style/TextAppearance.AppCompat.Title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:text="@{book.bookTitle}" android:layout_below="@+id/imageViewBookCover" android:layout_gravity="center_horizontal" android:layout_marginBottom="4dp" android:layout_marginTop="4dp" android:paddingLeft="4dp" android:paddingRight="4dp" tools:text="Giraffe"/> </RelativeLayout> </android.support.v7.widget.CardView> </LinearLayout> </layout>
3. In the adapter, set the click listener:
@Override public void onBindViewHolder(ViewHolder holder, int position) { ... holder.getBinding().setVariable(BR.click_listener, onClickListener); ... }
I ran into an issue whilst trying to set the Views Tag in XML due to a bug in the Android Data Binding Library (they did warn us it’s in beta 🙂 )
The following XML causes an issue with the Data Binding Library that will cause your app to not compile:
<android.support.v7.widget.CardView android:id="@+id/card_view" android:layout_width="@dimen/book_column_width" android:layout_height="wrap_content" android:layout_gravity="center" android:foreground="@drawable/book_selector" android:onClick="@{click_listener}" android:tag="@{book}" android:paddingBottom="8dp" app:cardCornerRadius="2dp" />
An explanation from StackOverflow as to why this wont work:
” When targeting devices pre-ICS, Android data binding takes over the tag of the outermost element of the layout. This tag is used for mostly for binding lifecycle and is used by DataBindingUtil.findBinding() and DataBindingUtil.getBinding() . “
Following the advice from the post and because I am targeting ICS and above, I used the mechanism specified in the answer:
Create a BindingAdapter to set the tag after the binding has occurred:
@BindingAdapter("specialTag") public static void setSpecialTag(View view, Object value) { view.setTag(value); }
Then in the XML, I can easily attach any object onto the View:
<android.support.v7.widget.CardView android:id="@+id/card_view" android:layout_width="@dimen/book_column_width" android:layout_height="wrap_content" android:layout_gravity="center" android:foreground="@drawable/book_selector" android:onClick="@{click_listener}" app:specialTag="@{book}" android:paddingBottom="8dp" app:cardCornerRadius="2dp" />
When the item is clicked we can get more information about the item by using the view.getTag() mechanism.
Two Way Binding
This is where things get tricky as the library does not work as expected. Unfortunately, binding an EditText to a String field in an object doesn’t yield the results we would hope for.
The following is a way to get two way binding working:
- In our BookDetail object we create an ObservableField<String> that will contain the books title. This will then inform anything that is watching of variable changes.
- In our XML, we will bind to that field on both the EditText and the TextView.
- We need to create a TextWatcher which will update the String field when the user types. It will notify the TextView of the changes.
public ObservableField<String> bookTitle = new ObservableField<>(); public TextWatcher watcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { if (!TextUtils.equals(bookTitle.get(), s.toString())) { bookTitle.set(s.toString()); } } };
<EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{book_info.bookTitle}" android:addTextChangedListener="@{book_info.watcher}" android:id="@+id/edit_text_book_name" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{book_info.bookTitle}" />
As you can see this is not an ideal solution. Firstly a lot of boiler plate code is required to get it to work and it’s not very elegant.
Better Solution
Fabio Collini has a great detailed post with his solution to this problem – by creating a custom binding for the property. In a few simple steps:
- Create a custom BindableString object
public class BindableString extends BaseObservable { private String value; public String get() { return value != null ? value : “”; } public void set(String value) { if (!TextUtils.equals(this.value, value)) { this.value = value; notifyChange(); } } public boolean isEmpty() { return value == null || value.isEmpty(); } }
- To get the text out of the BindableString object, we need to have a binding conversion. This instructs views on how to convert a BindableString into a String object:
@BindingConversion public static String convertBindableToString(BindableString bindableString) { return bindableString.get(); }
- Create a custom BindingAdapter to handle the text watching and setting:
@BindingAdapter({“app:binding”}) public static void bindEditText(EditText view, final BindableString bindableString) { if (view.getTag(R.id.binded) == null) { view.setTag(R.id.binded, true); view.addTextChangedListener(new TextWatcherAdapter() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { bindableString.set(s.toString()); } }); } String newValue = bindableString.get(); if (!view.getText().toString().equals(newValue)) { view.setText(newValue); } }
- Set the bindable string in XML.
<EditText android:layout_width="match_parent" android:layout_height="wrap_content" app:binding="@{book_info.bookTitle}" android:id="@+id/edit_text_book_name" />
- Change the BookDetail object to use a BindableString instead of the ObservableField<String>
public BookDetail { private BindableString bookTitle = new BindableString(); ... }
6. Two way binding should now work within your app.
Final Verdict
The Data Binding Library is a powerful tool. I suspect it will become more useful and more widely adopted. For now though, there are a few bugs and some kinks that need to be ironed out. After experimenting with it for a while, I am excited for what the library could potentially do in the future.
What are your thoughts on this library? Have you used it yet? Leave a comment below!
Leave a Reply
You must be logged in to post a comment.