Android Data Binding – Part 2

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();
    }
}
  1. 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.
  2. 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.
  3. 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.
  4. 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.

  1.  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:

  1. 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.
  2. In our XML, we will bind to that field on both the EditText and the TextView.
  3. 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:

  1. 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();
      }
    }
  2. 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();
        }
  3. 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);
      }
    }
  4. 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"
          />
  5. 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!

 


Comments

4 responses to “Android Data Binding – Part 2”

  1. This is a great write up on Data Binding. However, I just want to point out that it is not necessary to call `setVariable()` and `executePendingBindings()` as you have done in onBindViewHolder method. You can just call `viewDataBinding.setBook(bookDetail)`, for example.

  2. Rebecca Franks avatar
    Rebecca Franks

    Hi Igor,
    Thanks for your suggestion. It is possible to remove the ‘executePendingBindings()’ method call from this example. It was just stated in the docs that it might not be executed immediately, where as calling executePendingBindings() will execute it immediately. I guess it depends on your use case – might observe weird results on different devices.

    I also suspect this might be something that could change when the library moves out of beta.

  3. Andre Classen avatar
    Andre Classen

    Just found your write up while googling 😉 Really nice summary on Data Binding.
    After some testing i’ve fallen in love with it.
    So i decided to release a smaller app depending on data binding.
    Everything runs quite well, so i’m currently building a bigger app using it.
    In other words, i’ve completely switched to this overpowered lib.

Leave a Reply