Undabot logo
Undabot logo

RecyclerView — time to animate! (with payloads and DiffUtil)

Blog post

Throughout this post I will explain . The focus will be on the payloads and DiffUtil functionalities, as other topics are already covered in numerous online sources. After all, animation support is the main reason why RecyclerView was even introduced after ListView. Nevertheless, I still often see this potential underused.

Notifying adapter about changes made

Animations are defined through a component called ItemAnimator. RecyclerView developers were kind enough to provide us with some default animations defined in their own ItemAnimator implementation — DefaultItemAnimator. Item removal is animated by fading it out, insertion by fading it in, movement by translating it and item change by cross-fading the old view instance with the new one. To initiate these animations you have to notify the adapter how the data changed.

For rare cases when you don’t want to animate RecyclerView changes, use notifyDataSetChanged()

All other notify methods are used for animations. Using them, you can inform Adapter about exact changes that happened to the data — which items exactly should be animated as being added, removed or changed. These are:

Screenshot 2023-04-27 at 16.45.02.png

For example, if an item was added to the adapter’s data at position 5, you would call notifyItemInserted(5). Also, if items were removed from position 7 to position 9, you would call notifyItemRangeRemoved(7, 3). The following snippet shows usage examples of these methods.

Animations for these methods can be seen in README file of already mentioned accompanying RecyclerViewAnimations repository. If you take a closer look at the listed Change animation gif, you can see an unpleasant blinking animation. What actually happened is that the old item view instance was cross-faded with the new item view instance. Why did this happen when only info text could be changed?

When you think about it, actually nothing strange happened, because adapter was informed that something changed about that particular, but not what exactly changed. Obviously, you must supply an adapter with more information. To do that, you can use something called payloads.

Payloads

With payloads, granular item updates can be performed. This will result in a less aggressive and more efficient change animation. More efficient means that no extra view will be bound (or maybe even instantiated) to represent the new item view.

To provide information about what is different between the old and the new item state, two adapter methods can be used:

Screenshot 2023-04-27 at 16.47.51.png

As you can see, the payload is of type Any. This gives developer extra freedom on how he can use this payload functionality. How is that? Well, that’s because developers must handle this payload data on their own :) So how do you do it?

You already know about abstract fun onBindViewHolder(holder: VH, position: Int), which must be implemented in all adapter implementations. But on the inside, this method is actually called by its “big brother”, like this:

Screenshot 2023-04-27 at 16.47.57.png

As it can be seen, default implementation only calls the usual binding function, consequently completely ignoring payloads. Now it is time to override this method and use them!

Why the list of payloads? Didn’t you send a single payload object through that notifyItemChanged function and now there is a list? This is because binding will be done in the next frame draw pass. Until then, notifyItemChanged can be called multiple times for the same item, every time with a different payload.

So to change only that info message on a view, something like this should be implemented.

Here, InfoMessageChanged instance is sent as a payload. It serves simply as a flag to transfer information that the info message changed. In binding part, the payloads list is checked if empty. If it is, usual full bind is initiated by calling the super function. If not, a check is made on the passage of the flag and only then the view is modified.

Ways of using payloads data

In the example above, the payload was used as a simple flag. But what if notifyItemChanged was called multiple times and in the end, the newest infoMessage was actually the one from the start? Then nothing should be done. That is why I usually use data classes that consist of the old and the new data. That way, I can analyze if anything changed by taking the old data of the first payload and the new data of the last payload. And if there is a difference, the view update should be done.

So, long story short — use payloads in a way that suits you best.

Full list updates with DiffUtil

Sometimes things get complicated. That can be already seen in the upper moveItem example, where the data list must be carefully modified. More complicated examples include moving an item in a list, then removing some item before it, then maybe adding some item after it… The track of how exactly adapter should be notified is easily lost. Or maybe you want to do a full list update where some items may be the same or only partially changed? In order to animate all these changes, the new data must be compared with the old data and appropriate notify methods should be called.

Thankfully, Google’s Android team provided us with DiffUtil. This class calculates the difference between the two lists and outputs a list of update operations that should be executed in order to convert the first list into the second. In short — to provide us with animations :)

First of all, DiffUtil.Callback class must be subclassed and the following methods must be overridden:

Screenshot 2023-04-28 at 16.41.21.png

getOldListSize/getNewListSize are pretty straightforward — old/new list size must be returned.

areItemsTheSame must return true if the provided items from both the old and the new list are actually the same, that is, they denote the same item. This is usually done by giving each item a unique identifier which is then compared here.

areContentsTheSame is called only when areItemsTheSame returns true, meaning when the old item and the new item have the same unique identifier. What this method must return is whether some item property was changed when comparing the new and the old item contents.

getChangePayload is called only when areContentsTheSame returns false, meaning only when the old item and the new item have the same unique identifier, but one or more of their properties are different. Here you can once again use payload functionality to pass information about what is different between the old and the new item state. You are not obligated to override this method, but I highly suggest so in order to prevent that unpleasant blinking animation between the old and the new item view.

Once DiffUtil.Callback subclass is implemented, the diff result between the old and the new list can be calculated and applied to the adapter. This snippet shows an example of how DiffUtil can be used.

 In a snippet, TravelinoListDiffUtilCallback is an implementation of DiffUtil.Callback. getOldListSize, getNewListSize and areItemsTheSame are easily implemented. areContentsTheSame is also easy, thanks to Kotlin data classes. getChangePayload is also overridden pretty simple. All it is doing is sending the old item and the new item in a Change data class.

Sent payload is analyzed in the binding part. There, createCombinedPayload creates only one Change instance consisting of the initial state and the final state, ignoring the middle states, as they are not necessary for the view update. After that, the view is updated, if necessary.

Finally, setItems shows how DiffUtil.Callback implementation should be utilized and the data modified. The result of this code is a combination of insert, remove, move and change animations:

1_2b_3F22Sby_-iRNFZ7gx1w.gif

Done!

Thank you for reading this blog. I tried to keep it simple, but not too simple when compared to real life challenges. Hopefully this post will help your RecyclerView implementations be more user-friendly from now on. 

Similar blog posts

Get in touch