18

I am using a RecyclerView with ListAdapter (which uses AsyncListDiffer to calculate and animate changes when list is replaced).

The problem is that if I submit() some list, then re-order that list, and submit() again, nothing happens.

How do I force ListAdapter to evaluate this new list, even though it is "the same" (but order has changed)?

New findings:

I checked source code of submitList() and in the beggining there is a check:

public void submitList(@Nullable final List<T> newList) {
        final int runGeneration = ++this.mMaxScheduledGeneration;
        if (newList != this.mList) {

I think this is the problem. But how to get past it? I mean, surely the developers thought about submiting a different ordered list?

c0dehunter
  • 6,412
  • 16
  • 77
  • 139
  • @MDNaseemAshraf but that would defeat the whole purpose of using ListAdapter. ListAdapter automatically dispatches the minimum required changes to RecyclerView. – c0dehunter Mar 20 '19 at 14:45
  • Indeed. Did you try notifyItemRangeChanged(0,List.size()); ? Yeah, just realised this too will defeat the purpose of minimal change. – MD Naseem Ashraf Mar 20 '19 at 14:47
  • @MDNaseemAshraf are you familiar with `ListAdapter`? Because I am reading that we do not need to use any kind of `notify___` functions with it as it handles everything automatically. – c0dehunter Mar 20 '19 at 14:49
  • obviously without copy of old list you have nothing to compare with – Selvin Mar 20 '19 at 14:50
  • @Selvin, not sure I understand. `ListAdapter` saves old list and when you call `submit(newList)`, it makes comparison and notifyies RecyclerView which items changed. – c0dehunter Mar 20 '19 at 14:51
  • Since you've changed the order of the data set, will `ListAdapter` consider it as a new set. – Manoj Perumarath Mar 20 '19 at 14:51
  • @PrimožKralj saves(makes copy) or just keep references ? – Selvin Mar 20 '19 at 14:55
  • @Selvin, good point. There's the issue. – c0dehunter Mar 20 '19 at 15:11
  • Please check https://stackoverflow.com/a/53123659/6932487 Instead of submit null or new list (and lose all the meaning of the diffutils) you should use the dataobserver callback – Bobek Bobekos Oct 23 '19 at 19:00

8 Answers8

28

Instead of

submitList(mySameOldListThatIModified)

You have to submit a new list like this:

ArrayList newList = new ArrayList(oldList);
newList.add(somethingNew); // Or sort or do whatever you want
submitList(newList);

It's kind of a problem with the API. We would expect ListAdapter to keep a copy of the list, but it doesn't, probably for memory reasons. When you change your old list, you are actually changing the same list that ListAdapter has stored. When ListAdapter checks if (newList != this.mList) both newList and mList are referring to the same list instance, so no matter what you have changed on that list, it will equal itself, and ignore your update.

In kotlin you can create a new list via:

val newList = oldList.toMutableList() // Unintuitive way to copy a list
newList[0] = newList[0].copy(isFavourite = false) // Do whatever modifications you want
submitList(newList)

Note that you cannot do this:

newList.first().isFavourite = false

because that will also change the first item in your old list, and again ListAdapter won't see a difference between the first item in your old list and the first item in your new list. I would recommend that all items in your list have val properties exclusively, to avoid this problem.

Carson Holzheimer
  • 2,890
  • 25
  • 36
  • In the Kotlin example `newList` is immutable, so it cannot be changed. Also in the second line it is not possible to assign anything to `newList.first()`. This just returns the first item. – Lukasz Kalnik May 25 '21 at 13:29
  • 3
    Correct Kotlin example would be: `val newList = oldList.toMutableList(); newList[0] = newList.first().copy(isFavourite = false)` – Lukasz Kalnik May 25 '21 at 13:38
3

It defeats ListAdapter's purpose for automatically calculating and animating list changes when you call these lines consecutively:

submitList(null);
submitList(orderChangedList);

Meaning, you only cleared (null) the ListAdapter's currentList and then submitted ( .submitList()) a new List. Thus, no corresponding animation will be seen in this case but only a refresh of the entire RecyclerView.

Solution is to implement the .submitList( List<T> list) method inside your ListAdapter as follows:

public void submitList(@Nullable List<T> list) {
    mDiffer.submitList(list != null ? new ArrayList<>(list) : null);
}

This way you allow the ListAdapter to retain its currentList and have it "diffed" with the newList, thereby the calculated animations, as opposed to "diffing" with a null.

Note: However, no update animation will happen, if, of course, the newList contains the same items in the same order as the originalList.

aLL
  • 1,596
  • 3
  • 17
  • 30
  • This will not have any effect if the two subsequentlly submitted lists contain same elements in different order (this is the core issue, as described in the original question). – c0dehunter Mar 22 '19 at 12:44
  • 1
    even if the list was just re-ordered, as long as you instantiate a new arrayList using ```new ArrayList<>(YourReOrderedList)``` in your ListAdapter, the diffing will take effect unless the re-ordering process ended up with the same order as your OriginalList. – aLL Mar 22 '19 at 13:59
  • Actually you are correct. But two problems with this approach are that (for unknown reason) it takes 2-3 sec before any animation is seen AND, the recyclerview position is left intact (I want it to scroll back to top after "sorting", see this related [question of mine](https://stackoverflow.com/questions/55262409/recyclerview-scroll-to-top-with-asynclistdiffer-not-working)). – c0dehunter Mar 22 '19 at 14:34
  • 1
    The best way to scroll back to the top is here: https://stackoverflow.com/a/53123659/4672107 – Carson Holzheimer Sep 09 '19 at 05:47
1

To add to Carson's answer, which is a better way to do it, you can maintain the benefits of submitList as follows in Kotlin:

submitList(oldList.toList().toMutableList().let {
     it[index] = it[index].copy(property = newvalue) // To update a property on an item
     it.add(newItem) // To add a new item
     it.removeAt[index] // To remove an item
     // and so on....
     it
})
UnOrthodox
  • 106
  • 1
  • 4
1

I had a similar problem but the incorrect rending was caused by a combination of setHasFixedSize(true) and android:layout_height="wrap_content". For the first time the adapter was supplied with an empty list so the height never got updated and was 0. Anyway, this resoved my issue. Someone else might have the same problem and will think it is problem in the adapter.

android recyclerview listadapter example, RecyclerView ListAdapter. ListAdapter is a RecyclerView adapter that displays a list. This is available in RecyclerView 27.1+, and the same function also exists in the AsyncListDiffer class if you cannot extend the adapter. ListAdapter helps you to work with RecyclerViews that change the content over time. usersList.observe(this, list -> adapter.submitList(list)); recyclerView.setAdapter(​adapter); } } class UserAdapter extends ListAdapter<User,

all credits to this man: https://www.xspdf.com/help/50031492.html

Nurseyit Tursunkulov
  • 8,012
  • 12
  • 44
  • 78
1

The issue is that the new list submitted is not rendered.

androidx.recyclerview:recyclerview:1.2.0-beta02

The temporary solution is to smooth-scroll to any position after the new list committed, I do to the top.

movieListAdapter.submitList(list) {
    binding.recycler.smoothScrollToPosition(0)
}
Samnang CHEA
  • 643
  • 7
  • 10
1

If you have enable setHasFixedSize(true), remove this line.

"RecyclerView can perform several optimizations if it can know in advance that RecyclerView's size is not affected by the adapter contents...."

0

That function will not get called because ListAdapter won't consider it as another list, since it has all the same items only the order got changed.

@Override
public void submitList(@Nullable final List<T> list) {
super.submitList(list != null ? new ArrayList<>(list) : null);
}

So to solve this, you need to call this function with null first, then immediately call with the order changed list.

submitList(null);
submitList(orderChangedList);
Manoj Perumarath
  • 9,337
  • 8
  • 56
  • 77
  • 1
    Yes, this helps! I guess it was designed this way. – c0dehunter Mar 20 '19 at 15:10
  • 11
    This is incorrect, and also not how submitList is intended to be used. The actual problem is that `submitList` requires a new list to be submitted, not a reference to the same list that has been changed. Otherwise, as you change the list, you are also changing the list stored by the ListAdapter and when you submit it, it will compare the incoming list (the same list) and conclude that nothing has changed. See [this link](https://stackoverflow.com/questions/6536094/java-arraylist-copy) for making a copy of your list in Java or use `.toList()` in Kotlin – Carson Holzheimer Sep 09 '19 at 04:59
  • @Carson Holzheimer can you please elaborate on that and post as an answer – Manoj Perumarath Sep 09 '19 at 05:30
  • @Carson Holzheimer Does it have to be a deep copy or can a shallow copy be used? There are many answers provided in the link you gave, which one would you recommend? – AJW Nov 05 '21 at 20:09
  • @AJW A shallow copy will ensure that at least identities will be checked, so animations will be run to add/move/delete items. However you risk that the content of the cell isn't checked for changes if corresponding items in a list are represented by the same instance. I'd recommend deep copy, but only to the first level of the list, so middling? – Carson Holzheimer Nov 07 '21 at 10:04
  • @Carson Holzheimer Ok, gotcha. What would you recommend for Android method to make a deep copy? In your link above, what do you think of this approach: "newList.addAll(oldList.stream().map(s->s.clone()).collect(Collectors.toList()));" – AJW Nov 09 '21 at 21:37
  • @AJW yes that seems pretty safe for a Java solution. – Carson Holzheimer Nov 09 '21 at 23:52
-3

Just call listAdapter.notifyDataSetChanged() and the ListAdapter will redraw the list based on submitted values.

Rafa Araujo
  • 314
  • 1
  • 2
  • 9
  • 4
    entire point of using AsyncListDiffer is to not call notifyDataSetChanged unnecessarily. – Alex Sep 24 '20 at 22:00