androidkotlinandroid-recyclerviewlistadapterandroid-diffutils

ListAdapter DiffUtils newItem and oldItem the same when submitList() called


Just FYI, I'm not exactly looking for a 'fix' but for an explanation and a discussion that might help understand a little bit more how seemingly silly things like these work.

I was working on this bigger project when I realized that somewhere, a certain list wasn't being updated correctly. Looking a little closer, the items, were correctly being modified, and if you 'scrolled away' and back, the item's information would be displayed correctly.

I stumbled upon this article: ListAdapter not updating item in RecyclerView

But the difference here, is that in fact, DiffUtils was being called, but somehow the newItem and oldItem were the same! I understand that the library assumes you are using Room or any other ORM which offers a new async list every time it gets updated, but here's the thing. If I submit the list "naively" DiffUtils is not even called. But, if I submit the list as list.toMutableList() like some suggest then, DiffUtils IS called, but somehow the items, new and old, are already the same, hence, nothing gets updated at that moment (verified this by placing breakpoints inside areContentsTheSame).

I leave you here the relevant snippets and a link to a test project I created just so I could encapsulate the behavior and test it separately from everything else.

The Fragment - just calling the submitList

viewModel.items.observe(viewLifecycleOwner) {
    adapter.submitList(it.toMutableList())
}

ViewModel

private val _items = MutableLiveData<List<SimpleItem>>()
val items: LiveData<List<SimpleItem>>
    get() = _items

init {
    _items.value = ItemsRepo.getItems()
}

fun onItemClick(itemId: Int) {
    ItemsRepo.addItemCount(itemId)
    _items.value = ItemsRepo.getItems()
}

The "Repo" I create some data object ItemsRepo {

private var items = mutableListOf(
    SimpleItem(1),
    SimpleItem(2),
    SimpleItem(3),
    SimpleItem(4),
    SimpleItem(5)
)

fun getItems(): List<SimpleItem> {
    return items
}

fun addItemCount(itemId: Int) {
    items.find { it.itemId == itemId }?.let {
        it.itemClickCount += 1
    }
}

The GitHub repo: https://github.com/ellasaro/ListAdapterTest

Cheers!


Solution

  • Don't use mutable data classes or mutable lists with DiffUtil. It can lead to all kinds of problems. DiffUtil relies on comparing two lists, so if one of them is mutable and has been changed, it can't compare old and new successfully because there's no record of the previous state.

    I didn't take the time to narrow down your exact issue, but I bet if you change your Repo's getItems() to return items.toList() (so mutating the Repo doesn't mutate downstream lists), and change SimpleItem to be an immutable class, your problems will go away.

    Making SimpleItem immutable will be a little bit of hassle, unfortunately. The click listener instead of mutating the item will have to report back to the repo the id of the item that changed, and the repo must manually swap it out, and then you refresh the list.

    It will be cleaner if your Repo returns a Flow of lists that automatically emits when changes are reported to it. Then your ViewModel doesn't have to both report changes and then remember to manually query the list state again.

    I would use toList() and not toMutableList(). A mutable list communicates that you plan to mutate the list instead of just readding it, which you must never do with a list being passed to a DiffUtil.