Recyclerview++ with DiffUtil
DiffUtil is an AppCompat utility class developed to ease Recyclerview updates.
You just pass it the new dataset and current one and it automagically handles all the notifyItemRange*
events to provide cool RecyclerView animations or subtle item updates (like a single progressbar update instead of the whole item layout rebinding).
Principles
- Provide your custom DiffUtil.Callback fed with the old and new datasets to the DiffUtil.calculateDiff method, and obtain a DiffUtil.DiffResult which contains all the necessary operations (additions, deletions and optionnally moves) to get from old list to the new one.
- Update your dataset
- Call
diffResult.dispatchUpdatesTo(recyclerviewAdapter)
, your recyclerviewAdapter will receive all the corresponding notifyItemRange* events and benefit from shiny recyclerview animations!
It is strongly advised to run all dataset operations in the main thread, to ensure consistency. And for now (until next post), DiffUtil calculations will also be run in the main thread for consistency concern.
Basic usage
Here is a simple example of DiffUtil powered recyclerview update method in browsers adapter:
@MainThread
void update(final ArrayList<Item> newList) {
final DiffUtil.DiffResult result = DiffUtil.calculateDiff(
new MediaItemDiffCallback(mDataset, newList), false);
mDataset = newList;
result.dispatchUpdatesTo(MyAdapter.this);
}
You can see we only have these 3 basic steps.
With mDataset
the current dataset, newList
the new one, and false
the parameter value for moves detection, we don’t handle it for now.
The base DiffUtil.Callback implementation we use:
public class MediaItemDiffCallback extends DiffUtil.Callback {
protected Item[] oldList, newList;
public MediaItemDiffCallback(Item[] oldList, Item[] newList) {
this.oldList = oldList;
this.newList = newList;
}
@Override
public int getOldListSize() {
return oldList == null ? 0 : oldList.length;
}
@Override
public int getNewListSize() {
return newList == null ? 0 : newList.length;
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldList[oldItemPosition].equals(newList[newItemPosition]);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return true;
}
}
For now, areContentsTheSame
always returns true
, we’ll see later how to use it for smarter updates.
Let’s focus on areItemsTheSame
: this is simply a equals-ish method to determine if two instances correspond to the same model view.
Now, with this diffResult, for every item insertion and deletion we get the precise adapter notification.
Typical application is list feeding, all new items are nicely inserted or removed which makes fancy animations to happen, like if we had set all the notifyItemInserted
and notifyItemDeleted
one by one.
A nicer use case is list/grid filtering. Here’s the result, with that simple update(newList)
call:
Finer items updates
In video section, we want to update an item view if media progress or thumbnail has changed, but we don’t want to completely rebind its view.
Until VLC 2.0.6, you could experience a flicker of the video you were watching once getting back from video player.
That’s because media progress had changed and we blindly updated the whole view (which triggered thumbnail reloading).
Here is the DiffUtil.Callback specific to video grid which not only checks if items are the same, but if their content has changed also:
private class VideoItemDiffCallback extends DiffUtil.Callback {
List<Item> oldList, newList;
VideoItemDiffCallback(List<Item> oldList, List<Item> newList) {
this.oldList = oldList;
this.newList = newList;
}
@Override
public int getOldListSize() {
return oldList == null ? 0 : oldList.size();
}
@Override
public int getNewListSize() {
return newList == null ? 0 : newList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).equals(newList.get(newItemPosition));
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
Item oldItem = oldList.get(oldItemPosition);
Item newItem = newList.get(newItemPosition);
return oldItem.getTime() == newItem.getTime() &&
TextUtils.equals(oldItem.getArtworkMrl(), newItem.getArtworkMrl());
}
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
Item oldItem = oldList.get(oldItemPosition);
Item newItem = newList.get(newItemPosition);
if (oldItem.getTime() != newItem.getTime())
return UPDATE_TIME;
else
return UPDATE_THUMB;
}
}
If areItemsTheSame
returns true
, areContentsTheSame
is called for the same items. areContentsTheSame
returns false
if we detect small changes we want to propagate to the UI without totally rebinding the concerned item view.
In our case this is two Item
instances representing the same video file but artwork URL or time value has changed.
If areContentsTheSame
returns false
, getChangePayload
is called for the same items. We define here the updated value and return it. The dispatch util will call notifyItemChanged(position, payload)
with this value. Then it’s up to the adapter to manage the update:
In the adapter, we override onBindViewHolder(holder, position, payload)
to achieve this:
@Override
public void onBindViewHolder(ViewHolder holder, int position, List<Object> payloads) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position);
} else {
MediaWrapper media = mVideos.get(position);
for (Object data : payloads) {
switch ((int) data) {
case UPDATE_THUMB:
AsyncImageLoader.loadPicture(holder.thumbView, media);
break;
case UPDATE_TIME:
fillView(holder, media);
break;
}
}
}
}
onBindViewHolder(holder, position, payloads)
makes the smart move and updates the related view(s), instead of rebinding the whole item view. In our case, we load the thumb in the ImageView
or we regenerate the progress string and update the progress bar with fillView()
.
As the dataSet is already updated, we don’t need to pass the values here, so I opted for constants refering to the different actions.
DiffUtil process scheme
Moves detection
The DiffUtil.calculateDiff
algorithm can optionnally do a second pass to look for items movements between the old and new lists. If the sort order doesn’t change, it is useless to do it.
In VLC it’s only interesting in video grid for now, so we call:
DiffUtil.calculateDiff(new VideoItemDiffCallback(oldList, newList), detectMoves);
Where detectMoves
variable is a boolean which is set to true
only in case of video resorting call, in other cases we spare the second pass. Thanks to it, animations are fancier with only moving cards. Without this moves detection we’d get disappearing and reappearing cards.
Resorting videos with moves detection:
Without moves detection:
Going further
We now master diffutil, but we can do better and get calculation out of UI thread.
Rendez-vous to the second part of this post to check it out.
Leave a comment