AsyncListDiffer is not updating the recyclerview

8.3k Views Asked by At

I have a RecyclerView with an adapter that uses AssyncListDiffer. The problem I have is that the recyclerview is not updated when changes happen in the LiveData. The observer is notified but the list doesn't update.

This is my adapter:

class HourAdapter(private val interaction: HourInteraction? = null) :
RecyclerView.Adapter<HourAdapter.HourViewHolder>() {

    private val differ = AsyncListDiffer(this, DIFF_CALLBACK)

    fun submitList(list: List<Hour>?) {
        differ.submitList(list)
    }

    private fun getHourAt(position: Int): Hour {
        return differ.currentList[position]
    }

    override fun getItemCount(): Int {
        return differ.currentList.size
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HourViewHolder {...}

    override fun onBindViewHolder(holder: HourViewHolder, position: Int) {...}

    val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Hour>() {

      override fun areItemsTheSame(oldItem: Hour, newItem: Hour): Boolean {
        return (oldItem.name == newItem.name) && (oldItem.isChecked == newItem.isChecked)
        }

      override fun areContentsTheSame(oldItem: Hour, newItem: Hour): Boolean {
        return oldItem == newItem
       }

     }

    class HourViewHolder
        (
        internal val binder: HourItemBinding
    ) : RecyclerView.ViewHolder(binder.root)

}

I use the "submitList()" to submit the new list. But it doesn't work.

I looked for solutions online and basically there were 2 options:

  1. In the submitList function, call the "submitList" of AssyncListDiffer twice like this:

        differ.submitList(null)
        differ.submitList(list)
    }```
    
  2. The second option was to use ListAdapter and override the "submitList" function like this:
  override fun submitList(list: List<Hour>?) {
  super.submitList(list?.let { ArrayList(it) })
  }

The first solution works, but the recyclerview blinks whenever I update it. The second solution to override the method does not work for me.

I've been trying to fix this for days, but I can't make it work. Also, I don't want to use notifyItemChanged() or notifyDataSetChanged().

Is there another way?

3

There are 3 best solutions below

2
On BEST ANSWER

I came around the same thing and observed the following.

Each time AsyncListDiffer received my list; it was the same object as previously - present in memory. Hence, the differ decided nothing changed and did not submit the updated list.

My list contained one object inside, and for each submission attempt I was changing one field. The object and the list of course remained the same.

So, I wondered why option number 2 did not work, and turned out that I needed to be a little more expressive:

submitList(it.map {
    it.copy()
})

Otherwise, Kotlin would not make a deep copy of the object.

0
On

I was trying to delete a row and had the same problem, but solved it with the following:

The below code is added in the fragment:

private void deleteRow(int position) {
        ArrayList<Card> cardsArrayList = adapter.getArrayList();
        cardsArrayList.remove(position);
        adapter.submitList(cardsArrayList);
    }

And this code is in the adapter:

public ArrayList<Card> getArrayList(){
        List<Card> cardList = mDiffer.getCurrentList();
        return new ArrayList<>(cardList) ;
    }
0
On

The central question regarding why the AsyncListDiffer is not updating the items when a new list is submitted has been answered already by pratclot: modifying the previously-submitted list in memory and resubmitting it will not trigger the AsyncListDiffer to check if items are the same or not—it has to be a different list object.

However, I wanted to answer the other question, as I just ran into this issue myself and stumbled upon your question in trying to find a solution to the visual issue you shared:

The first solution works, but the recyclerview blinks whenever I update it.

As you mentioned in the comments to pratclot's answer, when I would submit a new copy of the list to the AsyncListDiffer, it results in just that one item disappearing & reappearing rather than the whole list. It is an unattractive look!

The reason for this is because by default, the RecyclerView has a RecyclerView.ItemAnimator active on it. This will animate the ViewHolders in four circumstances: added, removed, changed, and moved.

In this case, we are dealing with the changed animation. Therefore, the solution is to simply deactivate the animation when a change occurs to an item. If you have not given the RecyclerView a custom ItemAnimator, then you can simply cast it to DefaultItemAnimator to accomplish this:

(recyclerView.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false

If this does not work and the items still blink or fade out/in, it means that AsyncListDiffer.areItemsTheSame() logic is incorrect for your use case. If this returns false, the item will experience removing and adding animations. Only AsyncListDiffer.areContentsTheSame() equating to false will result in the changing animation.

Additionally, please note that you can modify ItemAnimator properties changeDuration, addDuration, moveDuration, and removeDuration to specify how quickly (in milliseconds) you want these animations to occur. By setting them all to 0, you can effectively remove all animations, not just the change ones. There may be certain use cases for such an approach.