4

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
  • 81
  • 1
  • 9

1 Answers1

4

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
  • 81
  • 1
  • 9