Data Binding and RecyclerView
In this post we try to achieve a few things. We want to understand more about Android Data Binding, and also apply it in a more realistic example. Our goal here is to cover the following:
- Take an existing code using Butterknife and replacing it completely with Android’s Data Binding.
- Show how to use RecyclerView’s Adapter and ViewHolder using Data Binding. The Adapter in this example can handle different ViewTypes.
- Show how to use <variable> in a layout xml.
- Show how <include> in our layout works together with Data Binding.
- Usage of lambda expressions in a layout xml.
Before and After
Seeing is believing and that’s why I want to show before we continue any further the before and after of our code. This will give you a better understanding of what we are tyring to achieve and make you decide if you want to continue reading any further.
Before using Data Binding our Adapter code for our RecyclerView will look like this
public void onBindViewHolder(final ForecastViewHolder holder, int position) {
boolean isCurrentDay;
ForecastDate night = forecastSparseArray.valueAt(position).getNight();
ForecastDate day = forecastSparseArray.valueAt(position).getDay();
switch (getItemViewType(position)){
case VIEWTYPE_CURRENT:
isCurrentDay = true;
break;
default:
isCurrentDay = false;
}
if(isCurrentDay){
holder.windMinNight.setText(night.getWindMin());
holder.windMaxNight.setText(night.getWindMax());
holder.windMinDay.setText(day.getWindMin());
holder.windMaxDay.setText(day.getWindMax());
holder.nightWeatherDesc.setText(night.getDescription());
holder.dayWeatherDesc.setText(day.getDescription());
holder.dayTempText.setText(day.getTempPhrase());
holder.nightTempText.setText(night.getTempPhrase());
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = holder.getAdapterPosition();
if(RecyclerView.NO_POSITION != position){
Forecast forecast = forecastSparseArray.valueAt(position);
clickHandler.onClickItem(forecast.getDate());
}
}
});
}
holder.tempMinNight.setText(night.getTempMinFormatted());
holder.tempMaxNight.setText(night.getTempMaxFormatted());
holder.tempMinDay.setText(day.getTempMinFormatted());
holder.tempMaxDay.setText(day.getTempMaxFormatted());
holder.dayTitle.setText(R.string.day);
holder.nightTitle.setText(R.string.night);
holder.dateTitle.setText(forecastSparseArray.valueAt(position).getFormattedDate());
}It is a lengthy onBindViewHolder method and depending on how many views you have for each item in the RecyclerView your implementation might be longer or shorter.
Now after using Data Binding our onBindViewHolder looks like this:
public void onBindViewHolder(final ForecastViewHolder holder, int position) {
boolean isCurrentDay;
Forecast forecast = forecastSparseArray.valueAt(position);
switch (getItemViewType(position)){
case VIEWTYPE_CURRENT:
isCurrentDay = true;
break;
default:
isCurrentDay = false;
}
if(isCurrentDay){
//Passing a Forecast to set its values back to the layout
holder.firstBinding.setForecast(forecast);
//we are setting this to enable onClick to refer directly to our Presenter using a lambda expression in the layout file
holder.firstBinding.setActionListener(actionListener);
holder.firstBinding.executePendingBindings();
return;
}
holder.binding.setForecast(forecast);
holder.binding.executePendingBindings();
}The final result is a much shorter implementation and more understandable code, and easier to mantain.
Now we can continue and explain how we achieved this improvement using Data Binding.
Understanding The Code
The first code block above is using a ViewHolder to bind all the necessary views with the data coming from our adapter. In this case the adapter is referring to the data using a SparseArray. It is important to mention that in order to populate the layout we need two objects (day, night) of type ForecastDate. Also getItemViewType(position) help us to identify if we are about to bind values for the first item in the RecyclerView. If this is the case then we bind the Views that appear only in the layout main_list_item_first.xml. Any other items in the RecyclerView will use the layout main_list_item.xml.
We are going to work directly with both layout xml files in the following section and it is important to know how we use those files.
Working With The Layout
Most of the work regarding binding values to our layout will be done in the xml files for each of the layouts that we need to use for our RecyclerView’s items.
The first item in our list will have a different layout than the rest. In this example we are working with a forecast weather app. This means that the first element in the RecyclerView will be the info of today or tomorrow. Thus we need a special layout because we want to show some extra info compared to the other elements in the RecyclerView.
The xml layout file for this first element in the RecyclerView looks like this:
Some observations before we continue.
It is important to put all your namespaces as attributes for your <layout> tag. This way you will avoid any errors in generating Data Binding classes.
We use <variable> to declare the Forecast object that we are going to use to bind its values to Views in this layout. Plus we have an ActionListener object that acts as our Presenter and it has the method of interest in order to see more details regarding an item that was clicked by the user. More details later in this post.
In this layout you will see that we have 2 <include> tags. These tags use the same layout list_item_first_day.xml.
Another important things to point out about these <include> tags they have as attributes app:title and app:date. Inside list_item_first_day.xml we have two other variables that are waiting to be set in order to bind its values and display the correct info. More about this other layout later.
As you can see the layout looks very similar to any other layout pre Data Binding. Now we can set the variables “forecast” and “actionListener” from the adapter.
Creating ViewHolders
Now we want to turn our attention on how to create ViewHolders, this is the pattern that RecyclerView uses to hold references to the Views in our layout. Now using Data Binding it will change our approach from what we already know, and it will reduce our code drastically.
First let me show you how onCreateViewHolder method looks like before and after Data Binding. There are more examples out there that apply better implementations and more efficient code, but for sake of clarity and demonstration I try to keep it as understandable as possible.
In our regular onCreateViewHolder we check the viewType and decide which layout to use for our ForecastViewHolder.
We have a bit more prepartion to do using Data Binding but the benefits are greater than adding a few more lines of code. As you will see later.
Data Binding will generate two binding classes (MainListItemFirstBinding, MainListItemBinding). These generated classes take their name from the layout name and camelcasing it. As you can see later the naming used for our layout files and view id’s are a bit long or might not represent best practices. When working in your project you might want to also put some thoughts in the naming conventions because it will be heavily use in Data Binding.
Note: If you want to change the generated class names you can always change them and assign your own name to it. You do this by defining it in your <data> tag and adding an attribute class to it.
<data class="MyBindingClassName">
...
</data>
You can read more in here https://developer.android.com/topic/libraries/data-binding/index.html
In our case we might need to apply this to make the names shorter for this classes.
Now we can proceed and modify our ViewHolder. The code will look something like this:
In this ViewHolder we use annotations from Butterknife in order to get all Views in the layout. We just let @BindView to know which one is the id in our layout that we are interested and we create a field for it in the ViewHolder.
As you can see this class can get big really fast depeding on how complex is your layout. Also you might need to search within your layout views and so on. As we showed previously the onBindViewHolder() will be in charge to set all these views with the necessary data coming from our two objects of type ForecastDate (day, night).
For our Data Binding ViewHolder we can implement it many ways. We need to understand that the generated Data Binding class that is created is per layout. We have two different layouts for this RecyclerView. We need a way to identify each binding class in order for us to bind the correct data in our adapter to its layout.
We could use generics and a method that can handle creating a ViewHolder that takes these two classes that we want to create. A good demo example that we found is the one added by Yigit Boyar:
public static <T extends ViewDataBinding> DataBoundViewHolder<T> create(ViewGroup parent,@LayoutRes int layoutId)
{
T binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), layoutId, parent, false);
return new DataBoundViewHolder<>(binding);
}
source: https://github.com/google/android-ui-toolkit-demos/blob/master/DataBinding/DataBoundRecyclerView/app/src/main/java/com/example/android/databoundrecyclerview/DataBoundViewHolder.java
For simplicty we have two binding class fields (firstBinding, binding) in our ViewHolder that we access when we want to bind to one of the layouts that we are working with.
Setting Values To The Layouts
Now we have all the pieces in place and we can move on to show how Data Binding will help make our code simpler and readable.
We have to move our attention back to onBindViewHolder and point out the key changes that put everything together. You can now go back to the beginning of the article and review both onBindViewHolder implementations that we presented there.
...
ForecastDate night = forecastSparseArray
.valueAt(position).getNight();
ForecastDate day = forecastSparseArray.valueAt(position).getDay();
...
holder.windMinNight.setText(night.getWindMin());
...
holder.dayWeatherDesc.setText(day.getDescription());
As you can see here night and day were necessary for us in order to properly set our views in a ViewHolder. With Data Binding this approach is something of the past and we actually set a Forecast object to our layout. Then the layout will take care of setting all the necessary views.
Forecast forecast = forecastSparseArray.valueAt(position);
...
holder.firstBinding.setForecast(forecast);
This is all we need to do in Java in order to set values in our layout. The interesting part now happens in the layout itself. Let’s have a look
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="forecast"
type="design.ivan.app.weatherrss.Model.Forecast"/>
<variable
name="actionListener"
type="design.ivan.app.weatherrss.MainScreen
.IMainContract.ActionListener"/>
</data>
...
</layout>
Once we add the tag <layout> and <data> we can start using DataBinding.
Now we can explain where the method setForecast(Forecast forecast) came from in onBindViewHolder. This is generated by the system when we added in the layout <variable name=”forecast”>. It will create a setter and getter for you by taking the name of the variable and adding get- or set- prefix to it. Once you add this variable in your layout it will be available in your Java code. If you can’t see it you might need to clean and build your project.
<TextView
android:id="@+id/item_date_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:textAppearance="@style/
TextAppearance.AppCompat.Title"
android:text="@{forecast.getFormattedDate()}" />
Above we can see how we can access and use this variable. We have access to all the getters corresponding to our Forecast object. Thus for this particular view we use getFormattedDate().
Not big mystery here, but if we have a bit more complex layout we might use <include> tags. And probably have different xml files with parts of the layout that we need to set. Data Binding also handles this situation.
<include
android:id="@+id/item_day"
layout="@layout/list_item_first_day"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@{@string/day}"
app:date="@{forecast.getDay()}"/>
We might have an include, and in order to set the views within its layout we can in fact pass variables to it. We simply need to add the app namespace and the name of the variable. In our case we have app:title and app:date
I don’t want to leave you assuming anything so let me show you how the layout file looks like in list_item_first_day.xml
As you can imagine we also need to add <layout> and <data> tags to this layout in order for Data Binding to work with this layout.
We saw how we passed two variables to this layout. Here we need to define those variables. One it is of type ForecastDate, and the other one is a String.
If you remember earlier our first version of our RecyclerView adapter needed to refer to two ForecastDate objects (day, night) in order to use our ViewHolder to set values in this layout. If you were paying attention we did exactly that when we added to our <include> the line date=”@{forecast.getDay()}”.
Because we are using this same xml file for both Day and Night layouts we need to specifically pass the correct ForecastDate object to each include, and set the correct title.
Data binding has some useful features. You can access your resources directly from data binding expressions. Above we passed the to the title variable in the include layout the title string coming from our string.xml file.
<include
...
app:title="@{@string/day}" />
And on the include’s layout we have
<data>
...
<variable name="title" type="String"/>
</data>
Where it is expecting a variable type String.
But this is not it. You can import classes, collections, and all sort of things. Things that then you can use in data binding expressions in your layout.
<data>
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>
<import type="java.util.List"/>
</data>
You can also add an alias in case that imported objects have same names.
Lambda Expressions
As you noticed earlier we had a onClickListener for our holder.itemView, but we completely got rid of it. Our layout uses a lambda expression to handle onClick events.
<layout>
<data>
<variable name="forecast" .../>
<variable
name="actionListener"
type=".weatherrss.MainScreen.IMainContract.ActionListener"/>
</data>
<LinearLayout
android:id="@+id/layout_list_item_first"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:clickable="true"
android:onClick="@{
()->actionListener.showPlaces(forecast.getDate())
}">
...
</LinearLayout>
This is possible if we have any events to listen to. In other words if we have an event attribute such as android:onClick. Then we can use a lambda expression that calls our actionListener variable that we can set in Java.
As you may recall onClick needs a method signature that matches
void public onClick(View view)
In our method showPlaces() we don’t need a View argument and instead we need to get the date from our forecast variable. Above you can see that it is not a problem we simple don’t pass any View argument and access the method getDate().
If for any reason we do need the View argument from onClick event then we can do the following
android:onClick="@{
(view)->actionListener
.showPlaces(view, forecast.getDate())
}"This is a valid lambda expression and it will work for this scenario. Also you don’t need to use view as the name of the argument. You can put whatever you want.
You can put any valid lambda expressions and it should work.
Note:
android:onCheckedChanged="@{
(cb, isChecked)->presenter.completeChanged(task, isChecked)
}"Events like onCheckedChanged where the event expects two parameters (View, boolean) and you need to access one of them. You need to use both parameters in the left side of your expression. And use the parameter that you need. You can’t leave one out.
android:onCheckedChanged="@{
(isChecked)->presenter.completeChanged(task, isChecked)
}"The code above will cause a compilation error.
I hope you find this information useful, and you have all the necessary info to start working with Android’s Data Binding.
I will leave you with some good links with even more information such as 2-way data binding, and more. George Mount and Yigit Boyar also have a very informative Google IO talk and a blog that you can follow with even more information:
https://halfthought.wordpress.com/2016/03/23/2-way-data-binding-on-android/
https://medium.com/google-developers/android-data-binding-adding-some-variability-1fe001b3abcc#.at9sdo8oi
https://www.youtube.com/watch?v=DAmMN7m3wLU&list=PLWz5rJ2EKKc8jQTUYvIfqA9lMvSGQWtte&index=54
https://developer.android.com/topic/libraries/data-binding/index.html