Three Lessons Learned From Using RecyclerView
Three Lessons Learned From Using RecyclerView
Stuck on the basics of RecyclerViews? Or do you want to learn how to filter, have multiple different view types in the RecyclerView? The author has been through it all, so here are some lessons he learned along the way.
Join the DZone community and get the full member experience.
Join For FreeThe general consensus among Android developers is that RecyclerViews, introduced in API 21, are what you should use where you might previously have gone with a simpler ListView. You get advantages like better layout managers, easier handling of large datasets and some nice animation capabilities. However, you're bound to hit some snags along the way. I had my fair share, which I'll share with you here. We'll start with a general introduction to the control, and move on to those lessons.
A Quick Intro to RecyclerView
If you haven't had the chance to create a RecyclerView yet, here are the basic steps you need to follow. It takes a little more work than ListView, but once you get your head around it, it makes sense.
Add Dependency in build.gradle
You will need to add RecyclerView into your build.gradle. There's actually a support library that allows you go back all the way to version 7. Add this line to your dependencies:
compile 'com.android.support:recyclerview-v7:25+'
Add RecyclerView in Your User Interface
Next, add in the RecyclerView in the XML for the fragment or activity that you are dealing with.
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/recycler_view"
android:scrollbars="vertical"
/>
In the fragment/activity, load up a reference to your RecyclerView as you would with any other control
RecyclerView recycler = view.findViewById(R.id.recycler_view);
Create an Adapter
Providing an adapter for the RecyclerView is how it will deal with rendering your dataset. There are two things you'll need to grasp here; RecyclerView.Adapter and RecyclerView.ViewHolder.
The adapter is a pretty simple structure. In summary, you will need to:
- Extend RecyclerView.Adapter<RecyclerView.ViewHolder>
- Store your data, in this case a list of POJOs called DataItem, typically passing through to your constructor.
- Override the getItemCount() method, which returns the number of items to be displayed
- Override the onCreateViewHolder method where you will create the ViewHolder for each item
- Override the onBindViewHolder which binds the data object with the ViewHolder. As you can see, a position parameter is provided, which allows you to retrieve the appropriate DataItem object from your list. This can then be passed through to the ViewHolder.
It would look as follows. I've left out the specifics of the ViewHolder for the time being.
public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
private List<DataItem> listData;
public MyAdapter(List<DataItem> data){
this.listData = data;
}
@Override
public int getItemCount() {
return listData.size();
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
//ViewHolder creation goes here
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
DataItem item = listData.get(position);
//ViewHolder data population goes here
}
}
Now let's dive into ViewHolders, which are basically a pattern where you have an object that contains both the View and the data to be rendered onto that same View. Typically these ViewHolders are defined as private classes inside of your Adapter.
All that you really need to do is create an object that extends RecyclerView.ViewHolder and store a reference to the view which is passed through on creation.
How you pass bind the data object to the ViewHolder is completely up to you. It is usually achieved by creating a simple setter method like setData below. Once the data is passed through, you can then manipulate the view as you would with any View.
private class MyViewHolder extends RecyclerView.ViewHolder{
//a reference to the View
private View itemView;
private MyViewHolder(View view){
super(view);
}
public void setData(DataItem item){
//place data from the object onto the view
TextField text = itemView.findViewById(R.id.item_text);
text.setText(item.getName());
}
}
Now back to the adapter, we can fill in those methods related to ViewHolders
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
//First inflate the view
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_view, parent, false);
//create the ViewHolder object
MyViewHolder viewHolder = new MyViewHolder(itemView);
return viewHolder;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
DataItem item = listData.get(position);
//ViewHolder data population goes here
((MyViewHolder)holder).setData(item);
}
In the create method, the view that we wanted to use (in this example item_view.xml) is inflated, and passed through to the ViewHolder. The bind method simply passes the object through.
Assigning a Layout Manager
Now that we have an adapter and ViewHolder in place, we can complete the creation of the RecyclerView in the Fragment/Activity.
List<DataItem> data = new ArrayList<>();
//populate your data list, or retrieve it from somewhere else
MyAdapter adapter = new MyAdapter(data);
recyclerView.setAdapter(adapter);
You can also assign any layout manager you wish. In this example I close a simple LinearLayoutManager
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
LinearLayoutManager displays items in a vertical or horizontal scrolling grid. You can also use GridLayoutManager to achieve a grid view, or StaggeredGridLayoutManager to use a staggered grid. These are just the built-in layout managers; you can always just create your own!
There's obviously a lot more that you can do with your RecyclerView, but the above should be enough to get you going. Onto the lessons!
Lesson 1: Be Careful With setHasStableIds()
I had the strangest problem where the content I was passing through to the adapter was in the correct order, and looked like it should display without issue. However, it seemed that some items were rendered with the incorrect view. It seemed that item 1 was being rendered a second time in position 5.
This lead to lots of debugging, and it wasn't until I stumbled across this question on StackOverflow that I realized where I had gone wrong. In my haste to integrate another library, I had added the following line in the constructor of my adapter:
setHasStableIds(true);
The reason I had added this? The widget required that all my items had stable IDs, so I just forced the adapter to think they did. It resulted in ViewHolders getting reused, which was certainly not my intention. Unless you're 100% sure what you're doing, don't add this line in!
Lesson 2: Multiple View Types Is (Fairly) Easy
The examples you'll find, including my tutorial above, cover the basic case pretty well; where you just have one type of view repeated for each item. There will inevitably be cases where you need to have completely different views for certain items.
This is really easy to do though. Building on the tutorial above, here are the steps to follow:
Add a way of distinguishing your objects. If your objects need a different representation on the UI add a way of differentiating these objects. This could be through a type variable for example. Here I've created two different types as constants. Naturally, there are lots of other ways to distinguish between types in Java!
public DataItem{
public static final int TYPE_ONE = 0;
public static final int TYPE_TWO = 1;
private int type;
public int getType(){
return type;
}
}
@Override
public int getItemViewType(int position){
if (listData != null) {
DataItem item = listData.get(position);
if (item != null) {
return item.getType();
}
}
//fallback
return 0;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
View itemView;
switch(viewType){
case DataItem.TYPE_ONE:
itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_type_one, parent, false);
TypeOneViewHolder viewHolder = new TypeOneViewHolder(itemView);
return viewHolder;
case DataItem.TYPE_TWO:
itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_type_two, parent, false);
TypeTwoViewHolder viewHolder = new TypeTwoViewHolder(itemView);
return viewHolder;
}
return null;
}
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
DataItem item = listData.get(position);
//work out odd-even color
if(item.getType() == DataItem.TYPE_ONE){
TypeOneViewHolder viewHolder = (TypeOneViewHolder)holder;
viewHolder.setData(item);
}
else{
TypeTwoViewHolder viewHolder = (TypeTwoViewHolder)holder;
viewHolder.setData(item);
}
}
Lesson 3: How to Filter Your List
Filtering is another straightforward task. Once you determine the criteria for what should be displayed, via a search filter or some other mechanism, you can call pass through these flags to your adapter. Typically, you will just add a filter method:
public void filter(boolean showNew, boolean showAll, List<DataItem> data){
//clear the local copy
this.listData.clear();
for(DataItem item: data){
if(showAll || (showNew && data.isNew()){
this.listData.add(item);
}
}
notifyDataSetChanged();
}
Some crucial points in this code:
- Make sure that your adapter stores a copy and not a reference to the original data. Otherwise, when filtering out for the adapter, you'll filter that original data set.
- In this example I pass through the full set of data as a parameter. You could also do this filtering seperately, and pass in the filtered data.
However, the most important part of all is where notifyDataSetChanged() is invoked, as this will cause the RecyclerAdapter to refresh.
Bonus Lesson: Consider Third-party Libraries
I wanted to get a few bonus features without too much work writing boilerplate code, so I used SuperRecyclerView. This gives you the ability to easily set views to use when the adapter is empty, progress bars while the adapter isn't set, infinite scrolling and easy refresh handling.
It's very clean to replace your existing RecyclerView:
<com.malinskiy.superrecyclerview.SuperRecyclerView
android:id="@+id/recycler_view"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_empty="@layout/emptyview"
app:layout_moreProgress="@layout/view_more_progress"
app:mainLayoutId="@layout/layout_recyclerview_verticalscroll"
app:recyclerClipToPadding="false"
app:recyclerPadding="0dp"
app:scrollbarStyle="outsideInset">
The view to use when there is no data available is expressed in the layout_empty property. In this example, you would need to create a view_more_progress.xml layout. Similarly, you would need to create a view_more_progress.xml view for the infinite scrolling capability.
Adding a refresh listener is as simple as getting your Fragment/Activity to implement SwipeRefreshLayout.OnRefreshListener. All you need to do is add an onRefresh method:
@Override
public void onRefresh() {
//get the data and when done, stop refreshing
recyclerView.setRefreshing(false);
}
Add this listener to the SuperRecyclerView, and specify the colors to use when animating the refresh progress bar.
recyclerView.setRefreshListener(this);
recyclerView.setRefreshingColorResources(android.R.color.holo_orange_light, android.R.color.holo_blue_light, android.R.color.holo_green_light, android.R.color.holo_red_light);
Opinions expressed by DZone contributors are their own.
{{ parent.title || parent.header.title}}
{{ parent.tldr }}
{{ parent.linkDescription }}
{{ parent.urlSource.name }}