DiffUtil ItemCallback areContentsTheSame() always returns true after updating an item on the ListAdapter
Asked Answered
F

5

15

While using a ListAdapter I noticed that after updating an item the DiffUtil.ItemCallback areContentsTheSame() method was always returning true. Debugging the code I realized that the old and the new item were exactly the same (the old state disappeared). Therefore the ListAdapter onBindViewHolder() method wasn't being called to update the respective row on the list (only the order of the items changed with a nice animation).

Checking other questions on Stackoverflow looks like it's a common issue that many developers have faced:

ListAdapter not updating item in reyclerview

ListAdapter not updating when editing content

DiffUtil.Callback not working as expected

Items in Recyclerview + Listadapter won't redraw on update

ListAdapter with DiffUtil.ItemCallback always considers objects the same

When submitting a new list to RecyclerView ListAdapter the diff check always returns true for areContentsTheSame()

However none of the above answers (if any) have provided a correct solution.

The only way it worked for me was by calling notifyDataSetChanged() on the ListAdapter everytime the observer emitted a new result.

But what's the whole point of using a ListAdapter and submitList() to notify changes if all the great performance you could get is thrown away by forcing the RecyclerView to redraw its content (all the items it has showed so far)?

  1. notifyDataSetChanged():

Even though it works, certainly is not the proper approach to go with if you decided to use a ListAdapter in first place.

  1. viewModel.getObjects().observe(this, listOfObjects -> listAdapter.submitList(new ArrayList<>(listOfObjects)));:

Didn't work.

After updating a item in the list I would like to see the respective changes taking place on the UI (not only the sorting order) for the corresponding row as expected.

Fairbanks answered 10/9, 2019 at 18:3 Comment(4)
same issue here, have you found solution?Sibilate
@Sibilate unfortunately not yet.Fairbanks
@Fairbanks how is it going? Faced into the same problem((Alcine
how you are updating your object? can you please share your codeBurgundy
S
11

I don't know if this is the solution for this specific case, but by the description it sounds exactly like something I went through very recently.

I am working with a new Room + LiveData + ViewModel + DiffUtils integration for the first time and I was having the same problem with updating the items in a RecyclerView after updating the list.

The problem I was having was due to my understanding of updating a list and allowing DiffUtils to do its job as it should. Hopefully the following will be clear enough:

What I was doing:

  1. User updates its item using a dialog
  2. The list item in the RecyclerView is updated with the new information
  3. Room triggers the LiveData observer because something might have changed
  4. The RecyclerView DiffUtils tries to check any differences between the old adapter list and the new one
  5. Nothing new was detected
  6. The Adapter is not triggered to update its UI

My mistake was thinking that the problem was in .5 which caused me to spend half a day going back and forth debugging the problem. Eventually I stumbled upon a SO question (cannot find it at the moment) which lead me to the correct source of the problem. That problem is really located in .2 - updating the item in the list.

This is the problem because we are updating our CURRENT adapter list with the new changes even before DiffUtils has a chance of comparing the old and new list changes. What this means is every single time the DiffUtils was always comparing the lists with the old list already containing the changes the new list had.

The solution? Do not update the list item since it is an object and list objects keep the same reference resulting in the same instance being updated everywhere that it is being used/referenced. Instead clone the item object (as in deep clone, I know this might be annoying), apply the changes the user made to that cloned object and then use that clone to update the item entry in the Room Database. DO NOT replace the adapter list item with the clone, leave the original one alone, we only want to update the information in the database, not the list since DiffUtils will take care of that.

What we are essentially doing here is creating an update payload for our Room Database, which will then trigger the LiveData observer into comparing the old list with the new one (containing the updated item data) resulting in the expected change detection between both lists.


In short;

Do this:

  • Deep clone adapter list item
  • Update only the clone with the new information
  • Update the database with the clone information

Don't do this:

  • Update the adapter list item directly
  • Update the database with the list item information

Syllabary answered 31/7, 2020 at 14:51 Comment(2)
yes deep copy is the key. i tried copy method generated by data class it does not worked either. so, created new instance with all value passed.Walls
This should be the accepted answerGuanabara
B
4

A few days ago, I have encountered the same issue. I was trying to update my object inside the adapter like this:

binding.accept.setOnClickListener {
            existingEntity.taskStatus = "accept"
            listener.onItemClick(existingEntity)
        }

The above code was updating the database object but not reflecting on my recyclerview. This happened because of the same reference of an object. So when I was updating the object inside my adapter, it automatically updated my object inside my list. So I changed my code like below:

binding.accept.setOnClickListener {
            val newEntity = existingEntity.copy()
            newEntity.taskStatus = "accept"
            listener.onItemClick(newEntity)
        }

So I created a copy object by using Kotlin data class copy() method and it worked for me.

Burgundy answered 8/5, 2021 at 2:34 Comment(0)
G
0

I spent a lot of time until I found out that I was having the same issue as described on this post. I tried many solutions, but in the end I realised that I was messing it up without knowing. If you're having this same issue, check if you aren't updating the UI first and calling a write command to update the value on your repository. In my case, I was updating the LiveData object and then updating that value on Firebase Database. The issue was being caused because my UI was listening to changes on that LiveData object, so my Firebase Database observer detected changes, the ListAdapter was called and it looked like nothing has been changed.

To solve the problem, I dropped the lines that were updating the LiveData object and I only called Firebase Database sending the changes.

Gina answered 11/2, 2020 at 22:55 Comment(0)
M
0

I have 0 reputation so I can't comment, but @Shadow's answer worked for me.

I am also working with Room + LiveData + ViewModel + DiffUtil and editing the contents of a list item with a dialog. Although java is pass by value, when we pass a value of an object were actually passing the reference to it. So when you edit the contents of the same object in say a dialog you're actually changing the same reference of the list item, hence why DiffUtil cannot calculate difference when LiveData does an onChange.

This explains java pass by value/reference better: Is Java "pass-by-reference" or "pass-by-value"?

What I did:

Item newItem = new ItemBuilder().setId(oldItem.getId()).......

Then make changes to newItem, then update the DB from viewModel

Millisent answered 1/8, 2020 at 20:7 Comment(0)
P
0

I am also facing this problem recently. For partial ViewHolder changes my approach is like this:

  • when a click is detected in list item of adapter -> delegate it to your view via callback

  • view will delegate this click to ViewModel with click position.

  • ViewModel will update the object property for that position. In my case, I have a List<FeedPost> list. Every FeedPost has user information, connect button, like count etc. So if user clicks on Like button, I will increment its value in my ViewModel like list.get(clickedPos).incrementLikeByOne().

  • Now updating the list in ViewModel is also going to reflect the change in your adapter list because the adapter list essentially contains the same object reference. When you are doing add() or addAll() you are not making a deep copy of list items. That's why DiffUtil fails in detecting partial view holder changes.

  • What I did was create a HashMap<Integer, Object> changeDetailsMap. This map will contain what data has changed in its value and key can be position and use this map inside areContentsTheSame() to return true/false in order to trigger partial ViewHolder changes. The reason I am using an Object so that I can pass anything I want (Integer, String, or my own POJO) but you have to take care of casting it correctly inside DiffUtil. Make sure whatever object you are putting in this HashMap it has an int field (let's say int classType so that you can cast it to correct Class)

  • One thing to note here is you have to use it with SingleLiveEvent like this: SingleLiveEvent<HashMap<Integer, String>> changeDetailsMap_SLE because it's a one-shot operation and not with MutableLiveData as it is sticky in nature. I was using fragment so when fragment gets popped from the backstack then ordinary LiveData again dispatches the recent value because it becomes active when fragment comes out from backstack. You can google SingleLiveEvent if you are not aware of it. You can use it for one-shot operation. Although, one of the googlers in his medium article suggested a better approach, but practically I find this SingleLiveEvent easy to use.

  • push this hashmap to view like: changeDetailsMap_SLE.setValue(changeDetailsMap). You can observe this SingleLiveEvent just like normal LiveData and pass it to your adapter and dispatch it to your DiffUtil. The way I am doing this in my adapter is like this

     public void updateConnectionStatus(Map<String, String>) {
         List<FeedAdapterModel> pseudoNewList = new ArrayList<>(adapterList); //adapterList is the list which you already have in your adapter class
         DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new FeedDiffUtil(adapterList, pseudoNewList, updatedConnectionStatusMap));
         adapterList.clear();
         adapterList.addAll(pseudoNewList);
         diffResult.dispatchUpdatesTo(this);
     }
    
  • Also, don't forget to clear this hashmap when work is done in the adapter. You don't want to get the old positions for which you ran DiffUtil earlier when you push again this SingleLiveEvent.

This approach also works where you are comparing list items (inside areItemsTheSame()) using equals() method and not using object property like oldList.get(oldItemPosition).getPostId() == newList.get(newItemPosition).getPostId(). I have a heterogeneous adapterList in my case so not all list items are guaranteed to have a postID so I can't rely on an object property comparison because it can be null.

Playback answered 20/11, 2020 at 15:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.