Animate RecyclerView items update

Animate RecyclerView items update

RecyclerView is common widget used in Android. It has build-in methods to handle insert, change, and removed. If you uses those methods, RecyclerView will animate it accordingly instead of refresh the whole list.

  • Insert: Items after the inserted item move down.
  • Update: Updated item changes content instead of view refresh.
  • Remove: Items after the removed item move up.

Prerequisite

  • Able to create a working RecyclerView

The old fashion way

RecyclerView.Adapter has notifyDataSetChanged(), notifyItemChanged(int), notifyItemInserted(int), notifyItemRemoved(int), notifyItemRangeChanged(int, int), notifyItemRangeInserted(int, int), notifyItemRangeRemoved(int, int).

notifyDataSetChanged() is the simplest way to notify the adapter that the data has changed. However, adapter do not know what has changed, so it will refresh all the views. Many programmer will just use notifyDataSetChanged() after updating the list.

In order to allow RecyclerView to animate item update, changes must be notified specifically. There are two ways to do it:

First one is to extend existing list class and dispatch changes on different methods. While this is the efficient to dispatch changes, it requires all the methods to be extended, including method like sort and filter.

Second one is comparing current list and new list to calculate the different and dispatch changes accordingly. While this is less efficient, it is a more generic way to handle all the cases.

However, both ways have to implement it yourself.

The new way

DiffUtil is added in version 25.1.0 which has implemented the second way for you.

class CustomAdapter : RecyclerView.Adapter<CustomViewHolder>() {
  private val items = listOf<CustomModel>()

  fun bind(items: List<CustomModel>) {
    val diffCallback = DiffableListCallback(this.items, items)
    val diff = DiffUtil.calculateDiff(diffCallback)
    diff.dispatchUpdatesTo(this)
  }

  // Other method implementations
}

DiffableListCallback is an implementation of DiffUtil.Callback which contains logic on how to compare the items.

interface Diffable {
    fun isSame(other: Any): Boolean
    fun isContentSame(other: Any): Boolean
}
class DiffableListCallback<T : Diffable>(
    private val oldList: List<T>,
    private val newList: List<T>
) : DiffUtil.Callback() {
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
        oldList[oldItemPosition].isSame(newList[newItemPosition])

    override fun getOldListSize(): Int = oldList.size

    override fun getNewListSize(): Int = newList.size

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
        oldList[oldItemPosition].isContentSame(newList[newItemPosition])
}

Two items are the same if they are refer to the same item. For example, in terms of database, it means they have the same primary key.

Two items are content same if they are equal in value. If two item are equal in value, it means there are no needs to update the item.

Therefore, Diffable delegates the comparison logic to the item. Combine Diffable with data class which implements value equality, you only need to implement isSame.

data class CustomModel(val id: Long, val text: String): Diffable {
  override fun isSame(other: Any): Boolean {
    val oth = other as? CustomModel ?: return false
    return id == oth.id
  }

  override fun isContentSame(other: Any): Boolean = this == other
}

After you have update the list, you can just update the items in the adapter and DiffUtil will calculate the changes for you.

val newItems = items.toMutableList()
newItems.add(CustomModel(99, "99"))
adapter.bind(newItems)