3

here is the situation:

I have a RecyclerView in a Fragment using DataBinding. Its adapter is a ListAdapter.

class MyFragment : Fragment() {
    override fun onCreateView(...) {
        val binding = // inflate layout with DataBindingUtil

        val myListAdapter = MyListAdapter()
        binding.recyclerView.adapter = myListAdapter

        val myViewModel: MyViewModel by activityViewModels()
        myViewModel.myLiveData.observe(viewLifecycleOwner, Observer {
            println("before submit: ${myListAdapter.currentList}")
            myListAdapter.submitList(it)
            println("after submit: ${myListAdapter.currentList}")
        })

        return binding.root
    }
}

Data to display is represented by a List<Double>. Once set, the list is used to initialize a MutableLiveData as follows:

class MyViewModel : ViewModel() {
    val data = listOf<Double>(/* init with some values */)
    val myLiveData = MutableLiveData(data)

    fun onButtonClicked() {
        update()
        println(myLiveData.value) // LiveData is correctly updated according to data
    }
}

Note: the update() method changes the list's values like: data[0] = someValue. It does not use clear() nor addAll() to update it.

  • In addition, I override ListAdapter.onCurrentListChanged() only to put a println() inside.

At this point, nothing peculiar. When the fragment is created, the RecyclerView shows the initial list. In Logcat:

I/System.out: before submit: []
I/System.out: onCurrentListChanged: [] -> [4.07, 6.29, 13.85, 17.92, 21.42, 23.47, 1.85]
I/System.out: after submit: [4.07, 6.29, 13.85, 17.92, 21.42, 23.47, 1.85]

For now, when I click on the button, the UI does not refresh because I did not assign a new list but only update the current one. Besides, if I navigate to another fragment then come back, the RecyclerView is displaying the updated values.

So I add the following at the end of onButtonClicked():

myLiveData.value = myLiveData.value

But this does not work, after clicking on the button the UI is still not updating automatically. Moreover, the Logcat prints only the 'before submit' and 'after submit', with the same ListAdapter.currentList value:

I/System.out: before submit: [4.88, 6.77, 13.85, 17.76, 20.93, 22.69, 1.85]
I/System.out: after submit: [4.88, 6.77, 13.85, 17.76, 20.93, 22.69, 1.85]

To me, prints should rather look like this:

I/System.out: before submit: [4.07, 6.29, 13.85, 17.92, 21.42, 23.47, 1.85]
I/System.out: onCurrentListChanged: [4.07, 6.29, 13.85, 17.92, 21.42, 23.47, 1.85] -> [4.88, 6.77, 13.85, 17.76, 20.93, 22.69, 1.85]
I/System.out: after submit: [4.88, 6.77, 13.85, 17.76, 20.93, 22.69, 1.85]

It appears that ListAdapter.currentList has already the updated value even before calling submitList(). I guess this is why the RecyclerView is not refreshed since the submitted list is the same as the current one.
I also tried the following at the end of onButtonClicked() with no success:

//myLiveData.value = myLiveData.value
myLiveData.value = emptyList()
myLiveData.value = data

And this...

//myLiveData.value = myLiveData.value
myLiveData.value = emptyList()
//myLiveData.value = data

... gives me that:

I/System.out: before submit: [4.88, 6.77, 13.85, 17.76, 20.93, 22.69, 1.85]
I/System.out: after submit: [4.88, 6.77, 13.85, 17.76, 20.93, 22.69, 1.85]
I/System.out: onCurrentListChanged: [4.88, 6.77, 13.85, 17.76, 20.93, 22.69, 1.85] -> []

Now onCurrentListChanged() is called but at the end of the 2 prints instead of in the middle.
Also, ListAdapter.currentList seems to contain values in the 'after submit' prints but is actually empty in the onCurrentListChanged() print. Confusing...

The only way I found to make the RecyclerView refresh is to call notifyDataSetChanged(). But then, there is no benefit to use ListAdapter and its submitList() method.

As a newbie, I may have done something not correctly but I do not know what.
The problem is only about the UI not refreshing. I can see in Locgcat that data is properly updating.

Thanks in advance

EDIT

class MyListAdapter() : ListAdapter<Double, MyViewHolder>(MyDiffCallBack()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        MyViewHolder.from(parent, itemCount)

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) =
        holder.bind(getItem(position))

    override fun onCurrentListChanged(previousList: MutableList<Double>, currentList: MutableList<Double>) {
        super.onCurrentListChanged(previousList, currentList)
        println("onCurrentListChanged: $previousList -> $currentList")
    }
}

class MyViewHolder private constructor(private val binding: MyItemViewBinding) : RecyclerView.ViewHolder(binding.root) {

    fun bind(data: Double) {
        binding.dataTxt.text = data.toString()
    }

    companion object {
        fun from(parent: ViewGroup, itemCount: Int): MyViewHolder {
            val binding = // inflate layout
            binding.root.layoutParams.height = parent.height / itemCount // set the height proportionally
            return MyViewHolder(binding)
        }
    }
}

class MyDiffCallBack : DiffUtil.ItemCallback<Double>() {
    override fun areItemsTheSame(oldItem: Double, newItem: Double): Boolean = oldItem == newItem

    override fun areContentsTheSame(oldItem: Double, newItem: Double): Boolean = oldItem == newItem

}
neige-i
  • 51
  • 8
  • Could you share the minimal representation of your recycler view adapter code? – Taseer May 28 '20 at 15:32
  • Are you using `DiffUtil` callback correctly? – Moshe Rabaev May 28 '20 at 15:39
  • @MosheRabaev I guess, I have edited my question by adding the Adapter/Holder/DiffUtil classes. – neige-i May 28 '20 at 15:46
  • In your `areItemsTheSame`, try changing `oldItem == newItem` to `oldItem.id == newItem.id` – Taseer May 28 '20 at 16:13
  • @ThunderCock But items are Double values. How can I do it ? – neige-i May 28 '20 at 16:14
  • `val myViewModel: MyViewModel by activityViewModels()` should be field – EpicPandaForce May 29 '20 at 07:31
  • @EpicPandaForce Why that? I mean, if I use myViewModel only inside onCreateView, I can just make it a local variable. Or will the ViewModel not work properly? In any case, this does not solve my problem. – neige-i May 29 '20 at 12:15
  • 1
    Same is happening with me After updating a **single Item** in LiveData List The adapter's list's contents are updated but the actual view is NOT – Saiprasad Prabhu Aug 14 '20 at 18:45
  • I am having the same issue with live data, I tried to convert it into a list and still not working. Did anybody else solve this? – Victor Cocuz Apr 28 '21 at 20:38
  • @VictorCocuz did not the answer work for you? – neige-i Apr 30 '21 at 06:32
  • In the end copying the list before submitting it to the adapter worked for me. I created a function like this: fun MutableList.getCopiedList(): MutableList { val list = mutableListOf() this.forEach { list.add(it.copy()) } return list } – Victor Cocuz May 03 '21 at 11:22

1 Answers1

2

It is a problem related to the difference between shallow and deep copies.
As stated in this article about ListAdapter:

Good to know is that the ListAdapter keeps a reference towards the provided list in submitList(). Always make sure to provide an other list with other item instances. If the references to the items are the same, DiffUtil compares the same items which are always equal, and the RecyclerView will not be updated.

neige-i
  • 51
  • 8