In this 360AnDev talk, we will discuss the overall architecture of the RecyclerView
: how it works, what are the priorities, and how you can get the most of it. We will also cover some best practices and common pitfalls.
Introduction (0:00)
My name is Yigit, and I work on the Android UI toolkit team. For the last two years, I have spent a lot of time working on the RecyclerView. Today I will talk about how to become a RecyclerView Pro. What does that mean?
I think for something like RecyclerView, there are two things.
- First, you need to understand how we got here. Why do we have RecyclerView? Because we also had ListView. How did this happen?
- The second thing is, you should know how to best use it. You should be very familiar with the best practices, and with the common mistakes.
Today’s talk will be in two parts. First, we will talk about how we arrived here, and then we will talk about a lot of different use cases.
Past & Present (0:47)
Part one. How did we get here? We had ListView, and ListView was designed 10 years ago, maybe even more. For example, a list where you have an icon, some text, and that keeps repeating. And sometimes it’s not that consistent. It might be expanding or have some elements that are visible or get hidden.
The problem ListView solves is that you’re out of data. You have a long list of something, and you don’t have that much memory, so you only want to show a small portion of this.
The way ListView fixes this problem is by faking it. It only shows the user the views that they can see. Most likely, the screen is not too big, and you only create views for these.
The whole idea is about faking, and as the user needs to move the screen, you create new elements, and while creating new elements, you actually reuse the existing views.
That makes it really efficient for a mobile phone; you lay out more views as necessary. But what happened to ListView over the past 10 years is like a lot of things that have changed in the operating system.
We want to have dividers, or we want to have selectability, all of this we want to have in transcript mode. All these things get added into the same class. ListView had a lot of undefined behavior because most of the time these things first happened in applications, and we tried to move these things into the framework.
Then this undefined behavior became an undefined API. Even though we don’t promise anything in the documentation about the behavior of ListView, people relied on this behavior, so we couldn’t change it anymore.
There was another problem with ListView. After it had been created, there were a lot of improvements in the framework as well. Things like FocusView, where the framework handles this very well, but ListView had its own idea of selection. Then you will see in the ListView codebase where it tries to synchronize these two things. The FocusView hopefully should be the selected view, but if they’re not the same, we try to pick the right one out of an undefined behavior.
Another good example is the ClickListener. ListView had this item, ClickListener, but the view also has a ClickListener. What’s the difference between adding an active ClickListener versus adding a ClickListener on your items?
And, of course, animations. When ListView was designed, animations didn’t even exist. Then the phones got stronger, and the UI paradigms changed. Now you want things to be interactive, so there was a lot of prototypes on top of ListView making this work. Some of them were promising, but a lot of it was regretful.
Another problem with ListView is that it’s really designed for a very specific use case. As people started asking for different layouts, like “I want the GridView.”, or Horizontal, or Staggered. All these things become really, really hard to implement on top of ListView because it has some assumptions on how things work. Even making it horizontal was hard.
You know when you have a problem that you cannot solve, what you do is you restart your computer. You say, “What happens if you reboot ListView? What would we do differently so that we don’t end up in the same place?”
The first idea was to elevate the best practices. Things like the ViewHolder. It became so common that every ListView implementation has one. Why don’t we make it a first class citizen of the API?
Then the second thing we wanted to do was decouple things. For example, the Recycler
. You could provide your view pool to RecyclerView, or we decoupled the view creation process from binding the view process, and if you remember the ListViewer adapter API there, it was just a GetView method. It will pass you the previous view. You had to add a tag for ViewHolder.
We got rid of all of it. There’s an API to create a view, and there’s another API to bind the view, and these two things are different.
I’ve also tried to do less. We tried to get rid of all the special focus handling inside the ListView. Instead, we had let the framework do what it does, and there’s always some edge cases a Layout Manager has to handle.
For example, you’re at the bottom of a ListView or a linear layout, and you want to go down. Now we know there are more views, so we lay out more views, but these are only HKs. The rest of the focus still works on the framework, and it works consistently.
The last thing we added was a smart adapter. If you have an adapter, you only tell us something has changed. It’s very hard to produce good output from that. We introduced adapters where you could tell us what has changed so that we can do better animations, better performances, and everything is just better.
You can see on slide 13 what the RecyclerView components look like. We have a:
- Layout Manager
- Item Animator
- Adapter
These are all main components. The Layout Manager is responsible for positioning the views. If there is a new requirement, a new UI paradigm that comes two years from today, it will be a Layout Manager.
We have the Item Animator that handles the animations because animations are also first class in RecyclerView, and we have the adapter that takes care of the views. RecyclerView is only in the center orchestrating these things.
As we improve RecyclerView we are not adding new APIs to the RecyclerView. We always add components, like we have the ItemTouchHelper. If you want to implement drag and drop and swipe, use ItemTouchHelper.
Everything else we add will be like this, so we are working on adding a SnapHelper for snapping. That’s a completely different component and doesn’t do anything inside RecyclerView.
Best practices (8:08)
The main section of this talk is the second part. I want to go through a lot of different examples. This is a little bit experimental. We’ll look at a lot of different use cases, what happens, why it happens, and what you should do. This is what makes you a pro.
The main thing is View::requestLayout
. This has nothing to do with RecyclerView; it is a part of the Android view system. And you know the view tree looks something like what you see on slide 16.
When you make some changes on a view, the view says, “You know what? I’m requesting a layout because something has changed.” It bubbles up to the parent until the root says, “Fine. In the next layout frame, I will call you back.” Any view that requests a layout, they just wait.
The next frame happens, and then the root reimburses to all of his children, “Measure yourself. Okay, this is your time to get your new layouts.” Everybody recursive measures them. Now, if a view didn’t request a layout, all of its measurements are cached. It’s a very cheap operation unless the specs change. We measure all the views, and when the layout comes, we reposition them.
Now the view hierarchy is again in a stable state. So what’s the implication on RecyclerView?
onBindViewHolder(ViewHolder vh, int position) {
....
imageLoader.loadImage(vh.image, user.profileUrl,
R.drawable.placeHolder);
}
It’s a very common onBindViewHolder
caller where you have an ImageView in your ItemView where you bind the URL into that, and your image loader is doing this work. There’s also a placeholder. Let’s look at a sample application on slide 20.
I bind all the views, all of the images had their placeholders because the image comes as synchronous, so you find out your image loader downloads it from the web, turns it into a bitmap, and calls ImageView, set image bitmap, and sets it.
When this happens, the image we use says, “Oops, my data has been invalidated. Let me request a layout.” It goes to the ImageView’s parent, which is your item view in this case, and it says, “Oh, one of my children has been invalidated. Let me request a layout.”
Now finally it comes to the RecyclerView, “Oh, one of my children has been invalidated. Let me request a layout.” It eventually comes to the RecyclerView to reposition all the views, which is an expensive operation.
You remember we have this thing setHasFixedSize
in RecyclerView. This would be used here. If RecyclerView has a fixed size, it knows that RecyclerView itself will not resize due to its children, so it doesn’t call request layout at all. It just handles the change itself. If invalidating whatever the parent is, the coordinator, layout, or whatever. But it’s only a limited optimization. It’s still an expensive thing to re-layout the RecyclerView.
The good news is that ImageView
s is from 2011 and I looked at the git logs. We change how it works when you update Drawable
. It checks, “Do I need to resize this for a Drawable change?” If it doesn’t need to resize, it doesn’t call requestLayout()
. It only calls invalidate. Then invalidate says, “I need to redraw my position. Everything else is all right. I just need to redraw.” It’s a very cheap operation.
But if you look at the TextView
, it says, “I have no idea what changed. Let me request a layout.” Even if you have set the same text, the exact same string on the TextView, and call that device, you will receive a requestLayout
because the internal representation of TextView is a lot more complex than the string you set.
There’s all this auto-linking going on, and TextView doesn’t take care of this. It simply requests a layout. This is something you should pay attention and try to avoid, because most of the time, even if you let TextView there, you probably know the size of it. It’s a single line.
How do I know this? It’s very simple. You go to Android Studio, put on the debug point inside the request layout method in RecyclerView, and if anything is calling that method, now we have the stick trace. You will know what to blame and you can work around the issue.
@Override
public void requestLayout() {
if (mEatRequestLayout == 0 && !mLayoutFrozen) {
super.requestLayout()
} else {
mLayoutRequestEaten = true;
}
}
Resizing items is an escalation of this problem. In the previous example, you made an unnecessary requestLayout
call, a bunch of unnecessary operations, but the layout looks fine.
Sometimes you’re not that lucky. Let’s look at this staggered grid layout on slide 29. We have the same image loading code, but what happens if one of these bitmaps is not in memory? Until that image is loaded, you want some sort of placeholder. The layout looks stable, but what actually happens is this.
I want you to pay attention to the clock on the following slides. The clock is on the right now. Essentially, the final layout is on the left. But because of the missing information, StaggeredGridLayoutManager
put it on the right because that first item says, “I have an ImageView with a height of zero.” StaggeredGrid is like, “Fine. I will just measure it this much.” And the other view gets positioned according to that.
This is bad. You want to avoid this, and the way to do it is to write your custom image view:
// AspectRatioImageView.java
private float mAspectRatio;
@Override
protected void onMeasure(int wSpec, int hSpec) {
int width = MeasureSpec.getSize(wSpec);
int height = (int) (width * mAspectRatio);
setMeasuredDimension(width, height);
}
It’s very simple. There’s a very specific solution for this case, but we have this AspectRatioImageView, which can size itself based on a known aspect ratio. Then we override the onMeasure
method, we get the view provided by the parent, calculate the height based on that, and set the measured dimensions.
When we get the actual image it will fit in this location; we know that, so ImageView will not request a layout, plus we get the correct information back to the Layout Managers to work around any problems.
Now there is a problem. How do you get the aspect ratio? If you are given an API like this:
{
"user" : {
"name" : "Michael",
"photoUrl" : "https://..."
}
}
You cannot work around that problem. I always try to convince people, if you are working with an API implemented in your company you should convince those people to provide you metadata when they pass you data.
This API would be a lot better if they send you the width and height so that you can calculate the aspect ratio:
{
"user" : {
"name" : "Michael",
"photoUrl" : {
"width" : 300,
"height" : 500,
"url" : "https://...",
"palette" : {}
}
}
}
You don’t need to fetch the actual image before laying it out properly. This is not only a problem in StaggeredGrid
. It’s even a problem in a linear layout because your views will stop jumping as you load the actual images. You should avoid that. Once you go into this metadata, they can even send you the palette of the image. The placeholder you put there is similar to the actual image.
You can also hit this problem if you are using data binding. When you update the data in the binding class, it waits until the next frame so that all of the changes are bound until the next frame, but RecyclerView doesn’t like this:
void onBindViewHolder(ViewHolder vh, int pos) {
vh.binding.setItem(items.get(pos));
vh.binding.executePendingBindings();
}
When RyclerView calls onBind
, it wants you to return the final view so we can measure and position it. All you have to do is call this method executePendingBindings
so it synchronizes all of the changes you make back to the view.
Data Updates (15:49)
Speaking of data updates, let’s say we have an example application, like the on slide 37, where we display a list of news, and you refresh the data. You get the new list of news from the server, and you update it. The update code looks something like this:
void onFetched(List<News> news) {
myAdapter.setNews(news);
myAdapter.notifyDataSetChanged();
}
Once you do this, it will look like this. It will instantly go to the new view. The views have been updated, but it’s not the best user experience. I’d do layout and jump into the new one.
If you look at the details, there’s something even worse happening there. When the RecyclerView is handling that data set change, you don’t tell us anything about what has changed, and RecyclerView goes, “Okay, I had this previous item at position zero, hopefully it’s still at position zero. Let me rebind it to position zero.” It goes, binds that item to new position zero, and does the same thing with item one and then two and three.
The first news “Sharks to Finals!”, we had nothing gridded. We bonded to this other news, and now we had the “Shake the Phone” news, now we bonded to sharks to finals. It’s a super inefficient operation. Then with all this TextView, you have invalidated everything.
Then you needed a new view to add the previous one that you already had but ; you misused, and you create a new ViewHolder for this. Now of course, there’s a very easy solution for this, you make your adapter have stable IDs:
long getItemId(int position) {
news.get(position).getId();
}
And probably if you have an application like this where news have IDs, you just implement this API, set my adapter as stable IDs, and once you do that, RecyclerView will nicely animate the view.
Now this looks good to the user, but there’s still something inefficient here because we have to rebind all these views. Although these are completely valid, we have to rebind that item back to itself.
Thanks to the stable ID, we knew what position to rebind, but we still had to do it. When we do that, of course, everything just invalidated themselves, and they have to be remeasured and relaid out. This is because the notifyDataSetChanged
with stable IDs only gives us enough information to reuse the correct ViewHolder.
To work around this issue we provide SortedList
, which is a simple collection implementation that keeps the elements sorted, and it has built-in logic to notify back the RecyclerView.
When you want to use a SortedList
, all you do is you implement this adapter callback:
SortedList<Item> mySortedList = new SortedList<Item>(Item.class,
new SortedListAdapterCallback<Item>(myAdapter) {
@Override
public int compare(Item item1, Item item2) {
return item1.id - item2.id;
}
@Override
public boolean areItemsTheSame(Item item1, Item item2) {
return item1.id == item2.id;
}
@Override
public boolean areContentsTheSame(Item oldItem, Item
newItem) {
return oldItem.text.equals(newItem.text);
}
});
There are three methods you have to override:
- One of them is compare, which simply tells which item I put first and the next one.
- The other one says, “Are these items the same items?” This is like the stable ID API. You simply compare the IDs of two items.
- The third one is when the items are the same.
By using this API SortedList
, you can say, “You know what, this news looks the same. I will not it tell anything about it.” RecyclerView can reuse the same ViewHolder without calling any other method. When you reuse ViewHolder as is it’s very cheap because all of the cache works:
void onFetched(List<News> newsList) {
mySortedList.addAll(newsList);
}
When you use SortedList
, all you do is, when you fetch the list of news, you add them to your SortedList
. That’s it. It takes care of notifying the adapter as the data changes.
Sort by vote (19:31)
Let’s say we wanted to sort our news by the number votes. To do that is very simple. You had one compare method. We will change that to compare instead by the number of votes. Now all of the items will be sorted by the number of votes:
SortedList<Item> mySortedList = new
SortedList<Item>(Item.class,
new
SortedListAdapterCallback<Item>(myAdapter) {
@Override
public int compare(Item item1, Item
item2) {
return item2.votes - item1.votes;
}
…
});
But let’s look at the internals of how SortedList
works. Let’s say we have this list of news, and we get the updated list of news, and in there we have this “To RX or Not To RX” news with the new vote count nine.
When you try to insert this into the SortedList
, it simply does a binary search. It goes to the middle item, is this above or below, above; goes up, above or below, below. It finds the right position and inserts it there. Now the item is being inserted in the right position, but we have a problem. We have just duplicated the item because SortedList
had no idea that this item is already in the list.
How do you work around this? SortedList
provides a method called updateItemAt
when you need to reorder items:
// SortedList::updateItemAt
Map<Integer, Item> items; // item id -> Item
void insert(Item item) {
Item existing = items.put(item.id, item);
if (existing == null) {
mySortedList.add(item);
} else {
int ind = mySortedList.indexOf(existing);
mySortedList.updateItemAt(ind, item);
}
}
We have to do a little bit more work because SortedList
only has this sorted list of items, it doesn’t have have any additional data. We will have a map that maps the item ID back to the item because item ID is stable. You need something stable that you can rely on. We’ll have our own insert method when we put this item into this map, and the map class returns the existing item back to you.
We know if the item already existed in the list, if the item didn’t exist in the list, it’s simple, we add it. But if the item was already in the list, we call this other method, find indexOf
it, and that tells SortedList
to update that item. Now it will recalculate the position of the item and also the space necessary to notify MS back to the adapter.
This works very well, but if you also have deletions you need to do a lot of bookkeeping. It’s not very straightforward. We have something better coming up for this.
DiffUtil (21:45)
In the next version of RecyclerView, we will have this new class called DiffUtil
, which is, very simply, you have two lists, and it calculates the difference for you. Magic. Let’s look at the API very briefly:
DiffResult result = DiffUtil.calculateDiff(new
MyCallback(oldList, newList));
myAdapter.setItems(newList);
result.dispatchUpdatesTo(myAdapter);
You call DiffUtil
; it has this calculateDiff
method, and you pass a callback, very similar to SortedList
. It will return your result. You update the list in your adapter; you set to the list, and you call this result that this dispatchUpdatesTo
method to dispatch all of the updates to the RecyclerView.
Now you get all of the benefits without almost doing nothing. Let’s look at the callback class:
class MyCallback extends DiffUtil.Callback {
@Override
public int getOldListSize() {
return mOld.size();
}
@Override
public int getNewListSize() {
return mNew.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return mOld.get(oldItemPosition).id == mNew.get(newItemPosition).id;
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){
return mOld.get(oldItemPosition).equals(mNew.get(newItemPosition));
}
}
There’s four methods you have to implement. There first two are simply get the size of the old list and get the size of the new list. Now we are doing, it actually doesn’t require you to have lists, you can have anything that provides random access, because this callback just works with positions.
You implement a very similar method which says, “Are these items the same?” It checks by ID, and you have this other one, “Are the contests the same?” It tell us whether there have been any changes on this item and from this, it’s going to dispatch all of the move, update removal, and all of the events for you to the adapter.
There’s also an optional API in this callback where you can say, “I can tell you what has changed in this item.”
@Override
@Nullable
public Object getChangePayload(int oldItemPosition,
int newItemPosition) {
Item oldItem = mOldItems.get(oldItemPosition);
Item newItem = mNewItems.get(newItemPosition);
if (oldItem.votes != newItem.votes) {
return VOTES;
}
return null;
}
If the votes have been updated, you can say, “Check the vote numbers of these two items.” and DiffUtil
will know to call this method if you told us these are the same item, but it has been changed. We ask you, “Okay, give me the change.” You can say, this vote is any random object that you can have in your code base.
@Override public void
onBindViewHolder(RecyclerView.ViewHolder holder,
int position,List<Object> payloads) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position);
} else {
if (payloads.contains(VOTES)) {
holder.voteCount.setText("" + item.votes);
}
}
}
The nice thing about this API is we will pass it back to the onBind
method in your adapter if and only if we reuse the same ViewHolder. Now with the onBind
method, the nice thing is you are given a ViewHolder that’s already representing that position. You know everything bound to there and the payloads include the list of things that has changed, so you can only update those things. This will be super efficient. You will do less work, and you can nicely animate based on these payloads.
The payloads will be empty if the ViewHolder is not reused for the same item. In that case, you run your regular bind logic. Otherwise, you look at, “Okay, if the payload contains the votes, let me change the vote count, and only change the vote count, because I know the other things then change.”
Resource management (24:35)
Another common question on RecyclerView is, “How do I do resource management? I have this expensive ViewHolders here and here.”
To understand this, you should know the lifecycle of a ViewHolder. On slide 60 we have a very simplified version. A ViewHolder is created via onCreate
, and soon after we will call it to bind it to a position. Once it’s bound to a position you can expect it to be attached very soon to the view. Once you receive this callback, you know that ViewHolder is on screen, probably visible to the user.
If the user is scrolling or something else happens, the item might be detached. For example, you’re scrolling, it went out of bounds, Layout Manager decided to remove that item, and we will call onViewDetachedFromWindow
.
At this point you know it’s still not attached, but the important thing is we may reattach it without calling any other method because we know this view still represents the same item. We can reuse it even though it has been removed and readded.
Let’s say you have a video playing feed items; you could stop the video and view as detach and restart when it is attached. It’s a nice way to understand how this item is becoming visible and invisible.
Now we may recycle a ViewHolder. Once we recycle a ViewHolder, that means we will always call onBind
. It may or may not be the same item. Probably it will be a different item, but we will always call onBind
.
When I recycle this code, it is a good time to release the expensive resources. If you had a bitmap, let’s release the reference to the bitmap so it can be reused. Because when onBind
will call it, you can reallocate all these things. In the onBind
method, in this case, you will acquire the expensive video resources because you have released them.
RecyclerView is async (26:35)
RecyclerView is asynchronous. What does this mean?
It doesn’t mean it is multithreaded. It only means that it handles stuff asynchronously. If you look at the example on slide 63, when a frame happens, RecyclerView makes all pending changes into the views and updates itself. After all, these changes have been updated, and the frame has finished. Anything else you call afterward, you can call scrollToPosition
. When the next frame comes, RecyclerView applies these changes.
This has some implications:
recyclerView.scrollToPosition(15);
int x = layoutManager.getFirstVisibleItemPosition();
For example, if you call RecyclerView scrollToPosition(15)
, and then you call Layout Manager, get me the first visible item, is this going to be 15? No, because we didn’t do that there. You told us, “Okay, I will scroll. Fine,” but we didn’t do it yet. If you expect it to happen instantly, it won’t. There are multiple reasons for this. One of these is this is very inefficient.
The second part is you want these things to happen together, so they look synchronous.
void onCreate(SavedInstanceState state) {
....
mRecyclerView.scrollToPosition(selectedPosition);
mRecyclerView.setAdapter(myAdapter);
}
If you are initializing your RecyclerView in your activity, in your onCreate
method, you could say scrollToPosition
selected position, and here is your adapter. Is this going to work?
The answer is yes because both setting the adapter and scrollToPosition will be handled in the next frame. It will work just fine.
Otherwise, if we’re trying to handle this synchronous when you call scrollToPosition, “Or you know what, I don’t have an adapter, sorry, continue.” We don’t want that to happen.
This has other side effects which are really good:
void onCreate(SavedInstanceState state) {
....
mRecyclerView.scrollToPosition(selectedPosition);
model.loadItems(items ->
mRecyclerView.setAdapter(
new ItemAdapter(items));
);
}
For example, the other use case, when you create an activity you usually don’t have the list. The list comes from the database which comes from the network that is asynchronous. You cannot set it up like that.
But you know the selected position because you opened the activity for some reason or it comes from a saved state. You can call saved instantly, and now you load items, assuming this is an asynchronous call. When the items are loaded you set the adapter.
Now is this going to work, because the next frame happened and we still don’t have the adapter?
The good news is that it will work, and the reason it works is slightly different. When you have a RecyclerView, and if you didn’t set an adapter and a Layout Manager, it skips all of the layout calls.
This is very useful because, again, in the same example, things like SavedInstanceState
, we get some information like latest scroll position instantly, but the data comes asynchronously. As long as you don’t initialize it, which means you don’t set an adapter or Layout Manager, everything else will look fine. As soon as you set the adapter, we will reuse that pending scroll position or all of the pending information we had about the RecyclerView.
ViewHolder++ (29:49)
Another thing is, earlier in the talk I mentioned we wanted to elevate best practices.
Now we force everybody to have a ViewHolder:
class ViewHolder {
TextView title;
TextView body;
ImageView icon;
}
void onBindViewHolder(ViewHolder vh, int pos)
{
Item item = items.get(pos);
title.setText(item.getTitle());
body.setText(item.getBody());
imageLoader.loadImage(icon, item.IconUrl());
}
But of course, when we do this by force, and so I call this a sad ViewHolder
because it has all the views, it finds view by ID ones which was the original idea behind the ViewHolder
.
Then you have your onBind
method which gets the item, gets the ViewHolder
and sets all the fields. I call this a sad ViewHolder because it is really disconnected.
Instead of doing this, I suggest as a best practice that you write your ViewHolders like this.
class ViewHolder {
...
public bindTo(Item item, ImageLoader imageLoader) {
title.setText(item.getTitle());
body.setText(item.getBody());
imageLoader.loadImage(icon, item.IconUrl());
}
}
void onBindViewHolder(ViewHolder vh, int position) {
vh.bindTo(items.get(position), mImageLoader);
}
Define your views again and create a bind method. I know this ViewHolder
can represent an item, and it also needs the image loader to work. You use this bind view method to set these things, and in your onBind
method in your adapter you give the item to it and everything else it needs.
Now this is much more abstract. You could use this ViewHolder
in other places in your applications, it’s like a presenter, and it works very well within itself.
View Types (31:06)
Another nice little thing with ViewHolders and ViewTypes is shown in another example:
@Override
public int getItemViewType(int position) {
User user = mItems.get(position);
if (user.isPremium()) {
return TYPE_PREMIUM;
}
return TYPE_BASIC;
}
In ListView, you could have ViewTypes, but you have to say, “I have five ViewTypes.” In RecyclerView this is different, you could have any number of ViewTypes. If you have this getItemViewType
method, we check to see if the user.isPremium
. If it’s premium, return to premium type. Otherwise, return to regular user type.
In our onCreateViewHolder
method we will implement something like this:
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup
parent, int viewType) {
View view;
switch (viewType) {
case TYPE_PREMIUM:
view = mLayoutInflater.inflate(R.layout.premium, parent,
false);
break;
case TYPE_BASIC:
view = mLayoutInflater.inflate(R.layout.basic, parent,
false);
break;
}
return new UserViewHolder(view);
}
We will check the viewType
. If this is premium, inflate this one, otherwise inflate this other layout, create the ViewHolder
and return. Now this is okay, but it’s not great because now we have two different code pieces that are supposed to work synchronously. You may break this. You have to take care of this very well.
We could do a nice little trick that works around this issue.
@Override
public int getItemViewType(int position) {
User user = mItems.get(position);
if (user.isPremium()) {
return TYPE_PREMIUM;
}
return TYPE_BASIC;
}
Instead of returning type, just return the layout. We know these layout numbers, they’re autogenerated, and they are guaranteed to be unique. Just return the layout.
Once you do this, if you go back to our onCreateViewHolder
method you can get rid of all of this logic.
Click Listeners (32:35)
Another sensitive topic in ListView of the RecyclerView is transition. When we released RecyclerView, someone asked this question on Stack Overflow, “Why doesn’t RecyclerView not have the item ClickListener?” It has almost 500 upvotes.
When I saw this, and it was during my early days in the team, I said, “Okay, I think we should implement this as soon as possible.” My boss was like, “No, no, no, no. We don’t do that.” I asked, “Why?” because I didn’t know. “Are we really bad people and we want them to suffer?”
No. There’s actually a reason behind it. The nice thing about the committee is that someone else replied and the reply has almost 800 upvotes. I took this as a compliment that what we did is the right thing.
The problem was the previous thing I mentioned about the duplicating function of the issue. If you add an ItemClickListener
on a ListView, then you are unable to click anything else inside those items. It’s very hidden. How would anybody think about this?
You will get all these Stack Overflow posts about, “Hey, ClickListener doesn’t work in my ListView, why?” This is why. We did something wrong and then we got rid of this.
What’s a nice way of doing a ClickListener in RecyclerView? There’s one major change.
class MyAdapter {
ItemClickListener itemClickListener;
public onCreateViewHolder(...) {
final ViewHolder vh = ....;
myViewHolder.itemView.setOnClickListener({
int pos = vh.getAdapterPosition();
if (pos != NO_POSITION) {
itemClickListener.onClick(items[pos]);
}
});
}
}
Instead of adapter, I want you to look at this class name. It’s not called ViewClickListener, it’s ItemClickListener
. Because if you have a list of something, and something is clicked by the user, you don’t care about the view, you care about the actual item.
Create this class, something says, it can be a callback class in your adapter that handles multiple action callbacks, or it can be, in this case, it’s a simple as ItemClickListener
.
In your onCreateViewHolder
, this is another important thing, when you create the ViewHolder, attach your ClickListener. When the click happens, you call ViewHolder.getAdapterPosition
. ViewHolder
has this method that it returns you the adapter position of that ViewHolder.
That is very important because, as the items might be moving in the adapter, we will not rebind that view, but you always have this getAdapterPosition
. By the time you bind it, it could be at an index of five. But by the time user clicked, it can be at an index of 10.
getAdapterPosition
returns you the correct index. Now you have to check this position against NO_POSITION
variable which is minus one because you may have deleted all the items from the adapter, but before RecyclerView could refresh the view, remember it’s asynchronous, before we could refresh it, a user might have clicked on the item. You check that against NO_POSITION
if it is not NO_POSITION
you call your ItemClickListener
with the item itself. It’s a much nicer well-defined API.
Adapter position vs layout position (35:42)
On slide 85 we have a layout with a list of items. AP stands for Adapter Position, LP stands for Layout Position. They’re actually the same most of the time.
But in your adapter, if you say, “I have moved position two to position five,” then this Adapter Positions and Layout Positions go out of sync because we know adapter context has been changed, but we have not yet recalculated the layout.
During this time, Layout Positions don’t match the Adapter Positions until the view system says, “Okay, you should layout yourself now. When we receive the callback, we will reposition the views,” and now they are synchronous again.
If you say, “Okay, even though I used which one, it’s kind of confusing,” it’s actually not that confusing. If you need to access the adapter, like the previous example of clicking an item, you want to access the items, so you want to access you adapter, use the Adapter Position.
But if you want to find, like what is the view before this one while a user clicked on this thing, then you want to use the Layout Positions. And internally, the Layout Managers only knows about Layout Positions, they don’t know about Adapter Positions. The rest of the callback uses the Adapter Positions.
Conclusion (37:11)
This is the last section. It’s my complaints :(. When I was preparing this talk I went through the public buganizer, the internal buganizer, Stack Overflow, and my emails. I tried to find all of these edge cases that I mentioned earlier. How do you do these things in RecyclerView? What are the things people got wrong and that we could do better?
But these ones that I will mention, they keep coming up. I don’t understand why. I have mentioned them in many other talks, but they keep coming up. So I’m going to mention them one more time.
First, this one is super sad. People create this thing called MySuperSolidRecyclerView
. They extend RecyclerView, they extend the onLayout
method, and they just rewrite this code.
class MySuperSolidRecyclerView extends RecyclerView {
public void onLayout() {
try {
super.onLayout();
} catch (Throwable t) {
// ignore
}
}
}
Try calling the super
. If it fails, eat the exception and continue. You cannot do this.
If we could do this inside RecyclerView, we wouldn’t throw the exception. If we are throwing an exception that means there is a problem. From my experience, the error is probably in your application and you are doing something wrong. Instead of doing this, go fix your application.
If you think the RecyclerView is doing something wrong, go and report the bug. What people are doing is they put this code in and two months later they start hitting another issue. They report that bug, and I’m looking at the bug report. How come RecyclerView ended up in this state?
That happens because they just ate a previous exception, so RecyclerView became unstable because we don’t clear things. Even the reference count and stuff inside RecyclerView, we don’t put this inside try
blocks because, if we hit an exception on main thread, it should die, that’s not recoverable.
Try to fix these things or provide a bug report.
What is the problem in this code?
public void onBindViewHolder(ViewHolder vh,
final int position) {
vh.likeButton.setOnClickListener = new
OnClickListener() {
items[position].liked = true;
notifyItemChanged(position);
}
}
Now I did this, setOnClickListener
. When something is clicked, change the item and notifyItemChanged
. You cannot do this.
The position we have provided it is not final. It used to be final in the ListView days. That’s gone. It’s not final. That’s why we provide you this getAdapterPosition
method. You cannot do that. Plus, also don’t do this. Don’t keep recreating an object every time we call onBind
.
This is why I suggest people to create their Listeners in onCreate
. Sometimes it’s not very appropriate, but in onCreate
you don’t have a position so you cannot make that mistake.
Even though I have seen someone, they send me a code in onCreate
, the integer variable, instead of ViewType, they named it position
. They thought that was the position. Sometimes this happens. But whatever this is, I know it’s how it used to work in ListView, but forget about it.
Another example I see that is very common is in onCreateViewHolder
method, if a type is a header type, check that the HeaderViewHolder
is null.
public void onCreateViewHolder(int type) {
if (type == HEADER) {
if (headerVH == null) {
headerVH = new HeaderViewHolder(...);
}
return headerVH;
}
}
If it is null, create it, and then return the HeaderViewHolder
. You cannot do this. The method is called Create
. Look at the description. It says, “Bring something into existence.” Did you bring anything into existence when you returned me the same ViewHolder?
The problem is that RecyclerView would not call the onCreate
method if it did not need a new ViewHolder. You are fighting against RecyclerView. You are defeating the purpose of the well-defined API.
Of course, you are probably wondering, “Well, I created this ViewHolder before, why did it get lost?” There is a method called onFail
the recycle. RecyclerView probably failed to recycle the ViewHolder, or it wanted two of them to run some animations.
You should look at the RecyclerView API, implement this onFail
the recycle method, and then do the right thing. See where it is failing and there is a nice way to recover these views. Don’t fight with the API.
The last thing, again, I see these people trying to fool the RecyclerView:
void refreshData() {
new AsyncTask(...) {
void doInBackground() {
List<Item> items = webservice.fetch();
adapter.setData(items);
adapter.notifyDataSetChanged();
}
}
}
They first write this code. I fetch new items, set the data on the adapter, and call notifyDataSetChanged
. But we say no, you cannot call notifyDataSetChanged
on a background thread. You have to call it on the main thread.
They’re like, “Cool. Sure. I will do the same and just move it to post execute. I will change my data on the background thread and notify on the main thread.”
void refreshData() {
new AsyncTask(...) {
void doInBackground() {
List<Item> items = webservice.fetch();
adapter.setData(items);
}
void onPostExecute() {
adapter.notifyDataSetChanged();
}
}
}
We are not bad people. If it was only about moving the code to the main thread we would do that for you. There is a reason for this.
As you change adapter data RecyclerView might be in the middle of laying out these items. For example, RecyclerView told the Layout Manager, “You have 100 items to layout,” then you deleted 50 of them.
While the Layout Manager is laying out items, it asks for the item at position 60, and responds, “Ooh, out of bounds exception,” because we don’t have that many items anymore. There’s a reason for this. You cannot do that. Don’t try to do that.
Calculate your data on a background thread, you should do that, but update the adapter on the main thread.
void refreshData() {
new AsyncTask(...) {
List<Item> doInBackground() {
List<Item> items = webservice.fetch();
return items;
}
void onPostExecute(List<Item> items) {
adapter.setData(items);
adapter.notifyDataSetChanged();
}
}
}
You can even do this with DiffUtil
. One of the reasons the new DiffUtil
class returns a result is that you could run it on a background thread. If you have thousands of items in your list, it may take a while to calculate the diff. You should run it on a background thread, and then you can pass the result back to the main thread and change your adapter data at the same time you do the special updates.
The important part is, while you are calculating it, you should not be modifying the data, otherwise the diff result will be invalid.
With this, I conclude my talk. Thank you. Since you went through this, here’s a little certificate of achievement on slide 103. Congratulations!
Q & A (40:58)
Q: If I’m writing a new app that ListView works for, should I use RecyclerView?
Yigit: No. If ListView is working for you, if you don’t need anything from RecyclerView, you can use ListView. We didn’t duplicate it; it’s still there.
Q: With DiffUtil
coming up, do you plan to deprecate notifyDataSetChanged
?
Yigit: I would be so happy if I could, but we cannot do that. There are still cases, like when your data might be coming back from a cursor bound to some other process, and you may not be caching it on your site. Even though DiffUtil
works based on indices, the whole idea of using a cursor is that you lazily get the items back, but DiffUtil
will have to create every single position. You’ll be accessing everything, and if you are going to access everything in the cursor, you are better off moving them to an actual list.
There are still some edge cases where you may want to use notifyDataSetChange
, but hopefully, there’s this bug report on the public buganizer that has almost 200 comments about people sending the wrong notifications and RecyclerView crashing, or sometimes RecyclerView do something wrong and crashing. Hopefully, I will get rid of those.
Receive news and updates from Realm straight to your inbox