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?

Solution 1

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.

Solution 2

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.

Solution 3

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())
        })

Solution 4

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.

Solution 5

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)

Solution 6

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.

Solution 7

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

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

Solution 9

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

Solution 10

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.

Solution 11

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.

Solution 12

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

Solution 13

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

Solution 14

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)
        }
    }

Solution 15

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.

Solution 16

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.

Solution 17

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.

Solution 18

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

Solution 19

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.

Solution 20

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.

Solution 21

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()

Solution 22

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)
}

Solution 23

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)

Solution 24

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)
}

Solution 25

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)
}

Solution 26

just call adapter.notifyDataSetChanged() after differ.submitList

Solution 27

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 :)

Solution 28

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())
}