161

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?

CoolMind
  • 26,736
  • 15
  • 188
  • 224
Veeresh Charantimath
  • 4,641
  • 5
  • 27
  • 36
  • The problem might be related to `ArtistsDiff` and thus to the implementation of `Artist` itself. – tynn Apr 09 '18 at 08:05
  • Yes, I too think the same, but I can't seem to pin point it – Veeresh Charantimath Apr 09 '18 at 08:07
  • You can debug it or add log statements. Also you could add the relevant code to the question. – tynn Apr 09 '18 at 09:03
  • also check this question, i solved it differently https://stackoverflow.com/questions/58232606/diffcallback-not-called-in-listadapter/58244523#58244523 – MisterCat Oct 05 '19 at 05:16
  • 2
    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. – Mukul Pathak Feb 24 '21 at 06:07
  • 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. – Mohdroid Jan 16 '22 at 08:15

29 Answers29

192

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.

insa_c
  • 2,851
  • 1
  • 16
  • 11
  • 13
    Because it requires the previous state to perform the diff. Of course it can't handle it if you overwrite the previous state. O_o – EpicPandaForce Apr 27 '18 at 12:26
  • 56
    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. – insa_c Apr 27 '18 at 15:25
  • 27
    So I am back to `RecyclerView.Adapter` and `notifyDataSetChanged()` . LIfe is good now. Wasted good number of hours – Udayaditya Barua Mar 19 '20 at 11:40
  • 10
    @insa_c 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 ... – Bencri Jun 23 '20 at 15:14
  • 1
    `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. – David Liu Sep 08 '20 at 04:20
  • I thought I'm missing something or there is some fault in my code. that felt bad :( tnx for your answer. – Reza Jan 27 '21 at 18:04
  • 3
    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. – RadekJ May 26 '21 at 07:03
  • 1
    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. – Jeffrey Blattman Mar 11 '22 at 21:45
116

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.

Kraigolas
  • 5,121
  • 3
  • 12
  • 37
usernotnull
  • 3,480
  • 4
  • 25
  • 40
  • That's precisely correct, but your second solution is also suggested in my answer :) – insa_c Apr 27 '18 at 15:27
  • 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 :) – usernotnull Apr 29 '18 at 08:04
  • 13
    That's a hack. Just pass a copy of the list. `.submitList(new ArrayList(list))` – Paul Woitaschek Aug 21 '18 at 13:48
  • 5
    I have spent the last hour trying to figure out what the issue is with my logic. Such a weird logic. – Jerry Okafor Oct 10 '18 at 13:04
  • 13
    @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. – usernotnull Oct 10 '18 at 13:20
  • 1
    @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 way – usernotnull Oct 10 '18 at 13:24
  • 2
    Even with using Room I’m running into a similar issue. – Bink Dec 19 '18 at 23:59
  • 6
    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? – Ralph Apr 10 '19 at 03:01
  • 1
    @Ralph make sure you override the equals() and hashcode() in your models and make sure that property (isSelected) is in those methods – usernotnull Apr 11 '19 at 12:11
  • @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 again – Ralph Apr 12 '19 at 02:52
  • 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. – Johann Dec 14 '19 at 11:58
  • @Ralph 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? – Cosmin Vacaru Sep 10 '20 at 13:26
  • @PaulWoitaschek this won't work. Because it is a shallow copy, not deep copy. – user175257 Feb 26 '21 at 07:53
  • @CosminVacaru I know this is late but, this is because the adapter references the same list whose items you are modifying. – Tayyab Mazhar Apr 13 '21 at 12:00
  • THANK YOU FOR THIS ANSWER! I've been trying to solve this since yesterday and your second solution finally did the trick! – Rus_o Sep 29 '21 at 10:51
  • @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? – AJW Nov 12 '21 at 13:40
  • @AJW 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. – usernotnull Nov 13 '21 at 08:54
  • In which place I can override this? `override fun submitList(list: List?) { super.submitList(list?.let { ArrayList(it) }) }` – MML Sep 07 '22 at 11:01
  • @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. – Vivek Gupta Dec 02 '22 at 17:58
46

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())
        })
Mina Samir
  • 1,527
  • 14
  • 15
  • 1
    That's weird but converting the list to mutableList works for me. Thanks! – Thanh-Nhon Nguyen Mar 26 '20 at 10:25
  • 4
    Why in hell is this working? It works but very curious why this happens. – March3April4 Apr 21 '20 at 07:45
  • 1
    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. @March3April4 – Mina Samir May 16 '20 at 01:38
  • Thanks. According to your comment, I guessed that the ListAdapter receives it's dataset as a form of List, 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. – March3April4 May 19 '20 at 06:33
  • I think you got it @March3April4 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 ;) – Mina Samir May 19 '20 at 09:20
  • 1
    This works because `.toMutableList()` creates a copy – Trevor Jul 30 '21 at 15:39
29

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.

Jan Veselý
  • 1,329
  • 1
  • 15
  • 24
  • 3
    Yeah, set the recycleview to wrap_content will update the list, if you set it to match_parent it will not call the adapter – Exel Staderlin Jun 17 '20 at 17:33
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

Yoann.G
  • 283
  • 3
  • 6
  • 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. – Mostafa Arian Nejad Sep 03 '19 at 18:51
  • Worked for me, Thanks @Yoann.G – Dev4Life Jun 09 '23 at 15:55
12

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...

Leonid Belyakov
  • 121
  • 1
  • 3
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.

piet.t
  • 11,718
  • 21
  • 43
  • 52
Ashu Tyagi
  • 1,400
  • 13
  • 25
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://stackoverflow.com/questions/49726385/listadapter-not-updating-item-in-reyclerview
 */
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.

Bojan P.
  • 942
  • 1
  • 10
  • 21
6

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

just_user
  • 11,769
  • 19
  • 90
  • 135
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.

protanvir993
  • 2,759
  • 1
  • 20
  • 17
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

ampalmer
  • 41
  • 1
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

gamedev-il
  • 41
  • 2
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.

Tomislav
  • 105
  • 6
2

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)
        }
    }
Zain
  • 37,492
  • 7
  • 60
  • 84
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)
}
Dharman
  • 30,962
  • 25
  • 85
  • 135
Shiv Shankar
  • 59
  • 1
  • 6
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.

Miguel Lasa
  • 470
  • 1
  • 7
  • 19
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.

March3April4
  • 2,131
  • 1
  • 18
  • 37
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.

John Glen
  • 771
  • 7
  • 24
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

Mohmmaed-Amleh
  • 383
  • 2
  • 11
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.

tonisives
  • 1,962
  • 1
  • 18
  • 17
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.

iCantC
  • 2,852
  • 1
  • 19
  • 34
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()

Mukul Pathak
  • 437
  • 6
  • 4
0

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)
}
Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
Stepan Kulagin
  • 465
  • 3
  • 8
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)
hyyou2010
  • 791
  • 11
  • 22
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)
}
Somwang Souksavatd
  • 4,947
  • 32
  • 30
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 :)

KamDroid
  • 102
  • 4
  • 7
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())
}
Alécio Carvalho
  • 13,481
  • 5
  • 68
  • 74
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())
ItSNeverLate
  • 533
  • 7
  • 8
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://stackoverflow.com/a/50031492/9735961

And yes, thank you, RecyclerView developers.

Samuel T. Chou
  • 521
  • 6
  • 31