ListAdapter not updating item in RecyclerView
Asked Answered
T

29

166

I'm using the new support library ListAdapter. Here's my code for the adapter

class ArtistsAdapter : ListAdapter<Artist, ArtistsAdapter.ViewHolder>(ArtistsDiff()) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(parent.inflate(R.layout.item_artist))
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        fun bind(artist: Artist) {
            itemView.artistDetails.text = artist.artistAlbums
                    .plus(" Albums")
                    .plus(" \u2022 ")
                    .plus(artist.artistTracks)
                    .plus(" Tracks")
            itemView.artistName.text = artist.artistCover
            itemView.artistCoverImage.loadURL(artist.artistCover)
        }
    }
}

I'm updating the adapter with

musicViewModel.getAllArtists().observe(this, Observer {
            it?.let {
                artistAdapter.submitList(it)
            }
        })

My diff class

class ArtistsDiff : DiffUtil.ItemCallback<Artist>() {
    override fun areItemsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
        return oldItem?.artistId == newItem?.artistId
    }

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

What's happening is when submitList is called the first time the adapter renders all the items, but when submitList is called again with updated object properties it does not re-render the view which has changed.

It re-renders the view as I scroll the list, which in turn calls bindView()

Also, I've noticed that calling adapter.notifyDatasSetChanged() after submit list renders the view with updated values, but I don't want to call notifyDataSetChanged() because the list adapter has diff utils built-in

Can anyone help me here?

Ticking answered 9/4, 2018 at 5:55 Comment(6)
The problem might be related to ArtistsDiff and thus to the implementation of Artist itself.Ledoux
Yes, I too think the same, but I can't seem to pin point itTicking
You can debug it or add log statements. Also you could add the relevant code to the question.Ledoux
also check this question, i solved it differently #58233106Villada
DiffUtil uses Eugene W. Myers's difference algorithm to calculate the minimal number of updates to convert one list into another. In short, this algorithm runs on two different lists. DiffUtil is used by AsyncListDiffer which runs the algorithm in the background thread and updates the recycler view on the main thread. AsyncListDiffer maintains a list itself as the previous list container because of which we have to provide a new instance of the list if we want an optimal performance out of Diffutils.Quartis
U should always pass new list to your LiveData. for example when u want to update articles, { getAllArtists.postValue(articlesList,toImmutableList()). That is all u need.Demoniac
T
199

Edit: I understand why this happens that wasn't my point. My point is that it at least needs to give a warning or call the notifyDataSetChanged() function. Because apparently I am calling the submitList(...) function for a reason. I am pretty sure people are trying to figure out what went wrong for hours until they figure out the submitList() ignores silently the call.

This is because of Googles weird logic. So if you pass the same list to the adapter it does not even call the DiffUtil.

public void submitList(final List<T> newList) {
    if (newList == mList) {
        // nothing to do
        return;
    }
....
}

I really don't understand the whole point of this ListAdapter if it can't handle changes on the same list. If you want to change the items on the list you pass to the ListAdapter and see the changes then either you need to create a deep copy of the list or you need to use regular RecyclerView with your own DiffUtill class.

Taps answered 25/4, 2018 at 21:18 Comment(8)
Because it requires the previous state to perform the diff. Of course it can't handle it if you overwrite the previous state. O_oPiton
Yes but at that point, there is a reason why I call the submitList, right? It should at least call the notifyDataSetChanged() instead of silently ignoring the call. I am pretty sure people are trying to figure out what went wrong for hours until they figure out the submitList() ignores silently the call.Taps
So I am back to RecyclerView.Adapter<VH> and notifyDataSetChanged() . LIfe is good now. Wasted good number of hoursReference
@Taps You can add 3 hours to your count, that's how much I wasted trying to understand why my listview wasn't updating in some edge cases ...Vanburen
notifyDataSetChanged() is expensive and would completely defeat the point of having a DiffUtil based implementation. You may be careful and intentful in calling submitList only with new data, but really that's just a performance trap.Strode
I thought I'm missing something or there is some fault in my code. that felt bad :( tnx for your answer.Jacinto
This is why mutable list are evil. Submitting the list, modifying it after and submitting the same instance? ouch! Why to submit the same list again? If you silently modified the list just call notifyDataSetChanged() to inform the adapter, there is no point of passing same reference again.Zischke
The idea that you need not only create a new list, but new list elements every time anything changes in the list is nuts. Just don't use ListAdapter, and manually call the varies notify item changed elements yourself. Sheehs.Minx
V
119

The library assumes you are using Room or any other ORM which offers a new async list every time it gets updated, so just calling submitList on it will work, and for sloppy developers, it prevents doing the calculations twice if the same list is called.

The accepted answer is correct, it offers the explanation but not the solution.

What you can do in case you're not using any such libraries is:

submitList(null);
submitList(myList);

Another solution would be to override submitList (which doesn't cause that quick blink) as such:

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

Or with Kotlin code:

override fun submitList(list: List<CatItem>?) {
    super.submitList(list?.let { ArrayList(it) })
}

Questionable logic but works perfectly. My preferred method is the second one because it doesn't cause each row to get an onBind call.

Venom answered 27/4, 2018 at 12:15 Comment(19)
That's precisely correct, but your second solution is also suggested in my answer :)Taps
I just gave a concrete example for anyone interested in how to solve it. One example for a quick fix and one to override the logic in the library. You gave the reason for the problem but assuming not everyone has good grasp of programming I just offered them the solution as well as why was it written like that by google :)Venom
That's a hack. Just pass a copy of the list. .submitList(new ArrayList(list))Countercurrent
I have spent the last hour trying to figure out what the issue is with my logic. Such a weird logic.Unsuccess
@PaulWoitaschek This is not a hack, this is using JAVA :) it's used to fix many problems in libraries where the developer is "sleeping". The reason why you'd choose this instead of passing .submitList(new ArrayList(list)) is because you may submit lists in multiple places in your code. You might forget to create a new array every time, that's why you override.Venom
@Po10cio It's weird mainly because when they wrote it like that, it was assumed it'll only be used with ORM libraries that offer new lists every time. If you're passing the same list but updated, you have to get around that, and that would be the best wayVenom
Even with using Room I’m running into a similar issue.Wilbertwilborn
Apparently this works when updating a list in viewmodel with new items, but when I update a property (boolean - isSelected) of an item in a list this still wont work.. Idk why but DiffUtil returns the same old and new item as I've checked. Any idea where the problem might occur?Forsyth
@Forsyth make sure you override the equals() and hashcode() in your models and make sure that property (isSelected) is in those methodsVenom
@RJFares thank you for the reply, but it's still not working on my current project. Will try to create a sample app and test againForsyth
My lists are different each time and areItemsTheSame does in fact get called. However, when areItemsTheSame returns false, I expect binding to a viewholder to take place. Even though it returns false for all but one of the items, onBindViewHolder only gets called for a single item - which makes no sense. As a result, my recyclerview only updates the one item. By using your solution, this solves the problem. This really appears to be a bug in the DiffUtils.Acidosis
@Forsyth i have the exact same scenario where in the new list i only change property "isSelected" and DiffUtil isn't working, not even with override of equals() and hash(). Did you find a solution?Riarial
@PaulWoitaschek this won't work. Because it is a shallow copy, not deep copy.Eufemiaeugen
@CosminVacaru I know this is late but, this is because the adapter references the same list whose items you are modifying.Alec
THANK YOU FOR THIS ANSWER! I've been trying to solve this since yesterday and your second solution finally did the trick!Fredelia
@UserNotNull I have seen other questions and answers mention that your second solution is a shallow copy and the preferred solution is to create a deep copy using clone(). Any thoughts on shallow copy vs. deep copy?Neuberger
@Neuberger I have left the android world a long time ago, but concerning shallow vs deep, if the solution works with shallow copying, no need to add complication with deep copying.Venom
In which place I can override this? override fun submitList(list: List<CatItem>?) { super.submitList(list?.let { ArrayList(it) }) }Turtle
@CosminVacaru also, Ralph , As TayyabMazhar said, only solution that worked me is by replacing old object present in the list to new object with different values.Bala
F
48

with Kotlin just you need to convert your list to new MutableList like this or another type of list according to your usage

.observe(this, Observer {
            adapter.submitList(it?.toMutableList())
        })
Fenland answered 24/9, 2019 at 12:14 Comment(6)
That's weird but converting the list to mutableList works for me. Thanks!Bennir
Why in hell is this working? It works but very curious why this happens.Fayum
in my opinion, the ListAdapter must do not about your list reference, so wit it?.toMutableList() you submit a new instance list to the adapter. I hope that clear enough for you. @FayumFenland
Thanks. According to your comment, I guessed that the ListAdapter receives it's dataset as a form of List<T>, which can be a mutable list, or even an immutable list. If i pass out an immutable list, the changes I've made is being blocked by the dataset itself, not by the ListAdapter.Fayum
I think you got it @Fayum Also, care about the mechanism you use with the diff utils because it also has responsibilities will calculate the items in the list should change or not ;)Fenland
This works because .toMutableList() creates a copyKagoshima
K
31

I had a similar problem but the incorrect rendering 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 resolved my issue. Someone else might have the same problem and will think it is problem with the adapter.

Karli answered 12/2, 2019 at 13:37 Comment(1)
Yeah, set the recycleview to wrap_content will update the list, if you set it to match_parent it will not call the adapterLaburnum
M
17

If you encounter some issues when using

recycler_view.setHasFixedSize(true)

you should definitly check this comment: https://github.com/thoughtbot/expandable-recycler-view/issues/53#issuecomment-362991531

It solved the issue on my side.

(Here is a screenshot of the comment as requested)

enter image description here

Microphysics answered 3/9, 2019 at 18:3 Comment(1)
A link to a solution is welcome, but please ensure your answer is useful without it: add context around the link so your fellow users will have some idea what it is and why it’s there, then quote the most relevant part of the page you're linking to in case the target page is unavailable.Principled
D
13

Wasted so much time to figure out the problem in same case.

But in my situation the problem was that i forgot to specify a layoutManager for my recyclerView: vRecyclerView.layoutManager = LinearLayoutManager(requireContext())

I hope no one will repeat my mistake...

Dinsdale answered 17/1, 2021 at 20:26 Comment(2)
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" is so much nicer nowadays :)Heterophony
Just want to upvote for this coz I just made the same silly mistake.Metachromatism
S
11

According to the official docs :

Whenever you call submitList it submits a new list to be diffed and displayed.

This is why whenever you call submitList on the previous (already submitted list), it does not calculate the Diff and does not notify the adapter for change in the dataset.

Sturgeon answered 15/1, 2019 at 7:1 Comment(0)
S
8

Today I also stumbled upon this "problem". With the help of insa_c's answer and RJFares's solution I made myself a Kotlin extension function:

/**
 * Update the [RecyclerView]'s [ListAdapter] with the provided list of items.
 *
 * Originally, [ListAdapter] will not update the view if the provided list is the same as
 * currently loaded one. This is by design as otherwise the provided DiffUtil.ItemCallback<T>
 * could never work - the [ListAdapter] must have the previous list if items to compare new
 * ones to using provided diff callback.
 * However, it's very convenient to call [ListAdapter.submitList] with the same list and expect
 * the view to be updated. This extension function handles this case by making a copy of the
 * list if the provided list is the same instance as currently loaded one.
 *
 * For more info see 'RJFares' and 'insa_c' answers on
 * https://mcmap.net/q/143916/-listadapter-not-updating-item-in-recyclerview
 */
fun <T, VH : RecyclerView.ViewHolder> ListAdapter<T, VH>.updateList(list: List<T>?) {
    // ListAdapter<>.submitList() contains (stripped):
    //  if (newList == mList) {
    //      // nothing to do
    //      return;
    //  }
    this.submitList(if (list == this.currentList) list.toList() else list)
}

which can then be used anywhere, e.g.:

viewModel.foundDevices.observe(this, Observer {
    binding.recyclerViewDevices.adapter.updateList(it)
})

and it only (and always) copies the list if it is the same as currently loaded one.

Soutane answered 7/7, 2019 at 8:56 Comment(0)
D
7

In my case I forgot to set the LayoutManager for the RecyclerView. The effect of that is the same as described above.

Diversiform answered 27/3, 2020 at 7:42 Comment(0)
I
6

I got some strange behavior. I'm using MutableList in LiveDate.

In kotlin, the following codes don't work:

mViewModel.products.observe(viewLifecycleOwner, {
    mAdapter.submitList(it)
})

But, when I change it to it.toList(), it works

mViewModel.products.observe(viewLifecycleOwner, {
    mAdapter.submitList(it.toList())
})

Although, "it" was the same list.

Ilailaire answered 3/8, 2021 at 15:19 Comment(1)
It's not the same list, it's a copy of the list. The toList() function returns a new list.Cristacristabel
W
4

I had a similar problem. The issue was in the Diff functions, which didn't adequately compare the items. Anyone with this issue, make sure your Diff functions (and by extension your data object classes) contain proper comparison definitions - i.e. comparing all fields which might be updated in the new item. For example in the original post

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

This function (potentially) does not do what it says on the label: it does not compare the contents of the two items - unless you have overridden the equals() function in the Artist class. In my case, I had not, and the definition of areContentsTheSame only checked one of the necessary fields, due to my oversight when implementing it. This is structural equality vs. referential equality, you can find more about it here

Womanlike answered 26/5, 2020 at 6:30 Comment(0)
P
3

The reason your ListAdapter .submitlist is not called is because the object you updated still holds the same adress in memory.

When you update an object with lets say .setText it changes the value in the original object.

So that when you check if object.id == object2.id it will return as the same because the both have a reference to the same location in memory.

The solution is to create a new object with the updated data and insert that in your list. Then submitList will be called and it will work correctly

Preteritive answered 30/12, 2020 at 17:37 Comment(2)
I made it work using this concept but did not actually know how it worked. Thanks for this explanation.Kingston
You are the god. No answers from the above solved my issue. You did a good job man.Stygian
C
3

It solve my problem. I think the best way is not to override submitList but add a new function to add new list.

    fun updateList(list: MutableList<ScaleDispBlock>?) {
        list?.let {
             val newList = ArrayList<ScaleDispBlock>(list)
            submitList(newList)
        }
    }
Cryobiology answered 26/2, 2021 at 11:13 Comment(0)
C
2

For me, this issue appeared if I was using RecyclerView inside of ScrollView with nestedScrollingEnabled="false" and RV height set to wrap_content.
The adapter updated properly and the bind function was called, but the items were not shown - the RecyclerView was stuck at its' original size.

Changing ScrollView to NestedScrollView fixed the issue.

Cornetcy answered 26/11, 2019 at 15:16 Comment(0)
B
2

I also ran into similar issue, my usecase was i had a clickHandler and item will be selected/not selected (toggle on click).

I tried most of the approach from the above answers, only thing that worked is

adapter.submitList(null)
adapter.submitList(modifiedList)

but problem with this is everytime i click on any clickHandler the whole list is being redrawn again which is very ineffecient.

What i did ?

I made a live data that will store last clicked item and observing that live data, we can tell adapter that live data has been updated like below

viewModel.lastClicked.observe(viewLifeCycleOwner, {
    adapter.notifyItemChanged(it)
}
Bercy answered 19/1, 2022 at 18:59 Comment(0)
N
2

Had a VERY similar issue, to this one, and decided to open a new thread and even create a GitHub project to mess around with. Most solutions didn't quite work for me, not even the toMutableList() way. In my case, the problem was solved by using immutable classes and submitting immutable Lists to the Adapter.

Nisen answered 5/2, 2022 at 18:37 Comment(0)
F
1

For anyone who's scenario is same as mine, I leave my solution, which I don't know why it's working, here.

The solution which worked for me was from @Mina Samir, which is submitting the list as a mutable list.

My Issue scenario :

-Loading a friend list inside a fragment.

  1. ActivityMain attaches the FragmentFriendList(Observes to the livedata of friend db items) and on the same time, requests a http request to the server to get all of my friend list.

  2. Update or insert the items from the http server.

  3. Every change ignites the onChanged callback of the livedata. But, when it's my first time launching the application, which means that there was nothing on my table, the submitList succeeds without any error of any kind, but nothing appears on the screen.

  4. However, when it's my second time launching the application, data are being loaded to the screen.

The solution is, as metioned above, submitting the list as a mutableList.

Fayum answered 21/4, 2020 at 7:58 Comment(0)
M
1

As has already been mentioned, you cannot submit a List with the same reference because the ListAdapter will see the lists are in the same location and will therefore not be able to use the DiffUtil.

The simplest solution would be to make a shallow copy of the list.

submitList(ArrayList(list))

Be wary converting the List to a MutableList, as that can create conditions for Exceptions and hard to find bugs.

Mcsweeney answered 22/9, 2021 at 18:45 Comment(0)
N
1

The way that worked for me is to override the submitList() and create a copy of the incoming list and each item inside it too:

override fun submitList(list: List<Item>?) {
    val listCopy =
        mutableListOf<Item>().apply {
            list?.map {
                add(Item(it.id, it.name, it.imageUrl))
            }
        }
    super.submitList(listCopy)
}
Nomination answered 27/9, 2021 at 12:7 Comment(0)
C
1

enter image description here

this will work .... what happen Is when you get the current list you are pointing to the same list at same location

Crispin answered 6/11, 2021 at 21:52 Comment(1)
What would the Java code be to create a new mutable list?Neuberger
S
0

I needed to modify my DiffUtils

override fun areContentsTheSame(oldItem: Vehicle, newItem: Vehicle): Boolean {

To actually return whether the contents are new, not just compare the id of the model.

Silverware answered 26/2, 2020 at 5:19 Comment(0)
F
0

Using @RJFares first answer updates the list successfully, but doesn't maintain the scroll state. The entire RecyclerView starts from 0th position. As a workaround, this is what I did:

   fun updateDataList(newList:List<String>){ //new list from DB or Network

     val tempList = dataList.toMutableList() // dataList is the old list
     tempList.addAll(newList)
     listAdapter.submitList(tempList) // Recyclerview Adapter Instance
     dataList = tempList

   }

This way, I'm able to maintain the scroll state of RecyclerView along with modified data.

Falito answered 11/5, 2020 at 10:3 Comment(0)
Q
0

Optimal Soltion: for Kotlin

        var list :ArrayList<BaseModel> = ArrayList(adapter.currentList)
        list.add(Item("Content"))
        adapter.submitList(list) {
            Log.e("ListAdaptor","List Updated Successfully")
        }

We should not maintain another base list as adapter.currentList will return a list in which diff is already calculated.

We have to provide a new instance every time a list updated because of DiffUtil As per android documentation DiffUtil is a utility class that calculates the difference between two lists and outputs a list of update operations that converts the first list into the second one. One list is already maintained by AsyncListDiffer which runs the diffutil on the background thread and another one has to be passed using adaptor.submitList()

Quartis answered 24/2, 2021 at 6:21 Comment(0)
J
0

I encounter a very similar issue.

After the data list changed, I submit it again, the recycler view doesn't show as I wanted. It shows duplicated items.

I haven't found the root cause, but I find a workaround, that is to set the adapter to recycler view again. I guess this makes recycler viewer forget the memory before and render again correctly.

userNftListFiltered = SOME_NEW_VALUE
binding.nftSendSearchList.adapter = searchNftAdapter //set adapter again
searchNftAdapter.submitList(userNftListFiltered)
Jurgen answered 28/10, 2021 at 18:57 Comment(0)
L
0

Once you have modify the array list, you have to let adapter know that which position that should be change

this code below is working in my case wish it may help

private fun addItem() {
  val index = myArrayList.size
  val position = myArrayList.size+1
  myArrayList.add(
    index, MyArrayClass("1", "Item Name")
  )
  myAdapter.notifyItemInserted(position) // in case of insert

  // in case of remove item
  // val index = myArrayList.size-1
  // myAdapter.notifyItemRemoved(index)
}
Loci answered 29/11, 2021 at 9:58 Comment(0)
R
0

In my case i was using same object(from adadptar) to update Room database. Create new object to update database and it'll fix the issue.

Example: I was doing this ->

val playlist = adapter.getItem(position)
playlist.name = "new name"
updatePlaylistObjectInRoomDatabase(playlist)

above code will change object in adapter before room database. So no change will be detected by DiffUtil callback.

Now doing this ->

val playlist = adapter.getItem(position)
val newPlaylist = Playlist()
newPlaylist.id = playlist.id
newPlaylist.name = "new name"
updatePlaylistObjectInRoomDatabase(newPlaylist)

Above code will not change anything in adapter list and will only change data in room database. so submitList will have different values DiffUtil callback can detect.

Enjoy the little things :)

Raddy answered 24/8, 2022 at 5:10 Comment(0)
P
0

This is something naturally expecte to be available on the official API, but as it isn't, this can be a way to deal with it:

fun <T, VH : RecyclerView.ViewHolder> ListAdapter<T, VH>.clearItems() {
    submitList(null)
    submitList(emptyList())
}
Proximal answered 24/8, 2022 at 10:59 Comment(0)
C
0

The adapter can not understand that you have some updates, I don't know why!? I am adding some entities to the list ad I m expected to collect them at the consumption point. But, nothing happens. As a solution that worked for me you can use the script below:

artistAdapter.submitList(it.toMutableList())
Christly answered 7/9, 2022 at 10:58 Comment(0)
Q
0

Because the problem lays inside the ListAdapter, I would like to solve it inside the ListAdapter.

Thanks to Kotlin extension, we can write it like:

class MyItemAdapter() :
    ListAdapter<Item, RecyclerView.ViewHolder>(ItemDiffCallback) {

    // ...

    override fun submitList(list: List<Item>?) {
        super.submitList(list?.toList())
    }
}

It does look like a tricky hack. So I'd like to make a comment too:

super.submitList(list?.toList()) // to make submitList work, new value MUST be a new list. https://mcmap.net/q/143916/-listadapter-not-updating-item-in-recyclerview

And yes, thank you, RecyclerView developers.

Quianaquibble answered 23/9, 2022 at 7:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.