0

Hey I am using diff util with ListAdapter. The updating of list works but I can only see those new values by scrolling the list, I need to view the updates even without recycling the view (when scrolling) just like notifyItemChanged(). I tried everything inside this answer ListAdapter not updating item in RecyclerView only working for me is notifyItemChanged or setting adapter again. I am adding some code. Please someone know how to fix this problem?

Data and Enum class

data class GroupKey(
    val type: Type,
    val abc: Abc? = null,
    val closeAt: String? = null
)

data class Group(
    val key: GroupKey,
    val value: MutableList<Item?> = ArrayDeque()
)

enum class Type{
  ONE,
  TWO
}

data class Abc(
    val qq: String? = null,
    val bb: String? = null,
    val rr: RType? = null,
    val id: String? = null
)

data class RType(
    val id: String? = null,
    val name: String? = null
)


data class Item(
    val text: String? = null,
    var abc: Abc? = null,
    val rr: rType? = null,
    val id: String? = null
)

viewmodel.kt

var list: MutableLiveData<MutableList<Group>?> = MutableLiveData(ArrayDeque())

 fun populateList(){
  // logic to call api 
   list.postValue(data)
 }


 fun addItemTop(){
  // logic to add item on top
  list.postValue(data)
 }

inside view model I am filling data by api call inside viewmodel function and return value to list. Also another function which item is inserting at top of list so that's why is used ArrayDeque

Now I am adding nested reyclerview diff util callback.

FirstAdapter.kt

class FirstAdapter :
    ListAdapter<Group, RecyclerView.ViewHolder>(comp) {

    companion object {
        private val comp = object : DiffUtil.ItemCallback<Group>() {
            override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean {
                return oldItem == newItem
            }

            override fun areContentsTheSame(oldItem: Group, newItem: Group): Boolean {
                return ((oldItem.value == newItem.value) && (oldItem.key == newItem.key))
            }
        }
    }
 ......... more function of adapter
}

FirstViewHolder

val adapter = SecondAdapter()
binding.recyclerView.adapter = adapter
adapter.submitList(item.value)

SecondAdapter.kt

class SecondAdapter : ListAdapter<Item, OutgoingMessagesViewHolder>(comp) {

    companion object {
        private val comp = object : DiffUtil.ItemCallback<Item>() {
            override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
                return ((oldItem.rr == newItem.rr) &&
                        (oldItem.text == oldItem.text) && (oldItem.abc == newItem.abc))
            }
        }
    }
 ..... more function
}

Activity.kt

 viewModel.list.observe(this, { value ->
            submitList(value)
 })

private fun submitList(list: MutableList<Group>?) {
        adapter?.submitList(list)
 //       adapter?.notifyDataSetChanged()
}

I am 100% sure that my list is updating and my observer is calling when my new list is added. I debug that through debug view. But problem is I can only see those new values by scrolling the list, I need to view the updates even without recycling the view (when scrolling) just like notifyItemChanged()

UPDATE

viewmodel.kt

class viewModel : BaseViewModel(){
    
 var list: MutableLiveData<MutableList<Group>?> = MutableLiveData()
//... more variables...


 fun fetchData(context: Context) {
        viewModelScope.launch {
            val response = retroitApiCall()
                response.handleResult(
                    onSuccess = { response ->
                                              
                    list.postValue(GroupData(response?.items, context))
                    },
                    onError = { error ->
                       Log.e("error" ,"$error")
                    }
                )
            }
        }
    }

 internal fun GroupData(items: List<CItem>?, context: Context): MutableList<Group> {
        val result: MutableList<Group> = MutableList()

        items?.iterator()?.forEach { item ->
        // adding item in list by add function and then return list.
        return result
    }


    private fun addItemOnTop(text: String) {
         list.value?.let { oldlist ->
          // logic to add items on top of oldlist variable
           if(top != null){
              oldlist.add(0,item)
           }else{
               val firstGroup = oldlist[0]
               firstGroup.value.add(item)
           }
          list.postValue(oldlist)
         }
    }
}

I am using sealed class something like this but not this one Example. And Something similar to these when call api Retrofit Example. Both link I am giving you example. What I am using in my viewmodel.

Kotlin Learner
  • 3,995
  • 6
  • 47
  • 127

1 Answers1

2

I don't know what's going on, but I can tell you two things that caught my attention.

First Adapter:

  override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean {
      return oldItem == newItem
  }

You're not comparing if the items are the same, you're comparing the items and their contents are the same. Don't you have an Id like you did in your second adapter?

I'd probably check oldItem.key == newItem.key.

Submitting the List

As indicated in the answer you linked, submitList has a very strange logic where it compares if the reference of the actual list is the same, and if it is, it does nothing.

In your question, you didn't show where the list comes from (it's observed through what appears to be liveData or RXJava), but the souce of where the list is constructed is not visible.

In other words:

// P S E U D O   C O D E
val item1 = ...
val item2 = ...
val list1 = mutableListOf(item1, item2)

adapter.submitList(list1) // works fine
item1.xxx = ""
adapter.submitList(list1) // doesn't work well.

WHY?

Unfortunately, submitList's source code shows us that if the reference to the list is the same, the diff is not calculated. This is really not on the adapter, but rather on AsyncListDiffer, used by ListAdapter internally. It is this differ's responsibility to trigger the calculation(s). But if the list references are the same, it doesn't, and it silently ignores it.

My suspicion is that you're not creating a new list. This rather undocumented and silent behavior hurts more than it helps, because more often than not, developers aren't expecting to duplicate a list supplied to an object whose purpose and promise is to offer the ability to "magically" (and more importantly, automatically) calculate its differences between the previous.

I understand why they did it, but I would have at the very least emitted a log WARNING, indicating you're supplying the same list. Or, if you want to avoid polluting the already polluted logCat, then at least be much more explicit about it in its official documentation.

The only hint is this simple phrase:

you can use submitList(List) when new lists are available.

The key here being the word new lists. So not the same list with new items, but simply a new List reference (regardless of whether the items are the same or not).

What should you try?

I'd start by modifying your submitList method:

private fun submitList(list: MutableList<Group>?) {
        adapter?.submitList(list.toMutableList())
}

For Java users out there:

adapter.submitList(new ArrayList(oldList));

The change is to create a copy of the list you receive: list.ToMutableList(). This way the AsyncListDiffer's check for list equality will return false and the code will continue.

UPDATE / DEBUG

Unfortunately, I don't know what is going on with your code; I assure you that ListAdapter works, as I use it myself on a daily basis; If you think you've found a case where there are problems with it, I suggest you create a small prototype and publish it on github or similar so we can reproduce it.

I would start by using debug/breakpoints in key areas:

  1. ViewModel; write down the reference fromthe list you "return".
  2. DiffUtil methods, is diffUtil being called?
  3. Your submitList() method, is the list reference the same as the one you had in your ViewModel?
  4. etc.

You need to dig a bit deeper until you find out who is not doing what.

On Deep vs Shallow copy and Java and whatever...

Please keep in mind, ListAdapter (through AsyncDiff) checks if the reference to the list is the same. In other words, if you have a list val x = mutableListOf(...) and you give this to the adapter, it will work the 1st time.

If you then modify the list...

val x = mutableListOf(...)
adapter.submitList(x)

x.clear()
adapter.submitList(x)

This will NOT WORK correctly, because to the eyes of the Adapter both lists are the same (they actually are the same list).

The fact that the list is mutable is irrelevant. (I still frown upon the mutable list; why does submitList accept a mutable list if you cannot mutate it and submit it again, escapes my knowledge but I would not have approved that Pull Request like so) It would have avoided most problems if they only took a non-mutable list, therefore implying you must supply a new list every time if you mutate it. Anyway...

as I was saying, duplicating a list is simple, in either Kotlin or Java there are multiple variations:

val newListWithSameContents = list1.toList() 
List newListWithSameContents = ArrayList(list1);

now if list1 has an item...

list1.add("hello")

When you copy list1 into newList... The reference to "Hello" (the string) is the same. If String were mutable (it's not, but assume it is), and you modified that string somehow... you would be modifying both strings at the same time or rather, the same string, referenced in both lists.

    data class Thing(var id: Int)

    val thing = Thing(1)
    val list1: MutableList<Thing> = mutableListOf(thing)
    val list2: MutableList<Thing> = list1.toMutableList()

    println(list1)
    println(list2)
// This prints
[Thing(id=1)]
[Thing(id=1)]

Now modify the thing...

    thing.id = 2
    
    println(list1)
    println(list2)

As expected, both lists, pointing to the same object:

[Thing(id=2)]
[Thing(id=2)]

This was a shallow copy because the items were not copied. They still point to the same thing in memory.

ListAdapter/DiffUtil do not care if the objects are the same in that regard (depending how you implemented your diffutil that is); but they certainly care if the lists are the same. As in the above example.

I hope this clarifies what is needed for ListAdapter to dispatch updates. If it fails to do so, then check if you're effectively doing the right thing.

Martin Marconcini
  • 26,875
  • 19
  • 106
  • 144
  • thanks for replying me. I tried this way as well. But it didn't work and I updated the viewmodel code. Please have a look. I added some piece of code what I am doing inside my viewModel. Thanks once again. – Kotlin Learner Oct 26 '21 at 20:17
  • Did you check that ? – Kotlin Learner Oct 27 '21 at 06:34
  • see updated answer. – Martin Marconcini Oct 28 '21 at 08:03
  • I'll add code in github and tag you whenever is ready thanks – Kotlin Learner Oct 28 '21 at 09:22
  • @Martin Marconcini For Java, what method would you recommend for making a copy of the list? Would the copy need to be a deep copy or could it be a shallow copy? – AJW Nov 09 '21 at 20:52
  • @AJW `new ArrayList(oldList);` ;) (shallow copy, what the AsyncDiffUtil checks is for the list reference. Meaning `oldList == newList`. Not if the items are the same, that's what the diff util calculates later. – Martin Marconcini Nov 11 '21 at 10:11
  • @MartinMarconcini can you please see this [issue](https://stackoverflow.com/q/69935721/11560810) – Kotlin Learner Nov 11 '21 at 23:19
  • @Martin Marconcini I thought that a shallow copy does not change the reference with the newList, so others have commented that a deep copy with Java cloning is required. Any thoughts on shallow vs. deep copy? – AJW Nov 12 '21 at 13:32
  • What the code is looking for is the *reference* of the list. Is it the same collection in memory? (not the items, not the reference of the items, but the actual collection). It's naturally implied by design that if two list references point to the same list in memory, that they contain the same items (this is hopefully obvious). What ListAdapter (well AsyncDiff) does is compares if *the lists are the same list*, not the contents; the contents of the lists are only compared if they are effectively different lists otherwise it assumes there's no point in comparing the same thing. – Martin Marconcini Nov 12 '21 at 13:51
  • @Martin Marconcini Ok, so wouldn't new ArrayList(oldList) create a new list that has the same reference as the old List? Others have suggested the only way to make a new list with a different reference is to clone the old list...basically make a deep copy of the oldList. What do you think? – AJW Nov 23 '21 at 04:31
  • 1
    There are two references to keep track of: The List (1) and Each item in the list (n). `AsyncDiff` checks for equality *in the actual list and not the items*. So yes, you need `new ArrayList(oldList)` This gives you a new reference to a new list, whose items are the exact same reference as the old list; if you check the references of `oldList` and the `new` list, you'll see they are different. If you *modify* an item in either list, it will be modified in both. If you *remove* an item from one list, *it will not be removed* from the other. – Martin Marconcini Nov 23 '21 at 09:43
  • @MartinMarconcini Sorry for too late to add [Github](https://github.com/vivek-modi/DiffUtilExample) Link. I Opened [issue](https://stackoverflow.com/q/70095578/11560810) – Kotlin Learner Nov 24 '21 at 11:51