DiffUtil not refreshing view in Observer call android kotlin - android

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.

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:
ViewModel; write down the reference fromthe list you "return".
DiffUtil methods, is diffUtil being called?
Your submitList() method, is the list reference the same as the one you had in your ViewModel?
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.

Related

How can DiffUtil know about list changes when only the companion object has changed?

As shown in the image, I would like the unit of the Detail item to be changed at once according to the toggle button.
Detail list items were set as companion objects because it was determined that it was not necessary to have a unit property individually.
However, it seems that DiffUtil determines that there is no change between the new list and the old list, perhaps because the unit property is set as a companion object.
So there is no update of the view either.
How can I make DiffUtil responsive while changing the companion object?
Detail
#Entity
data class Detail(
#PrimaryKey(autoGenerate = true)
var id: Int,
val set: Int,
var weight: String = "",
var reps: String = "") {
companion object {
var title: String = ""
var unit: String = "kg"
val memo = ""
}
}
ViewModel
class DetailViewModel(application: Application) : ViewModel() {
private val repository: DetailRepository
private val _items: MutableLiveData<List<Detail>> = MutableLiveData()
val items = _items
private val list: List<Detail>
get() = _items.value ?: emptyList()
init {
val detailDao = DetailDatabase.getDatabase(application)!!.detailDao()
repository = DetailRepository(detailDao)
}
fun changeUnit(unit: String) {
Detail.unit = unit
if(list == null)
return
_items.postValue(list) // To notify the observer.
}
fun addDetail() {
viewModelScope.launch(Dispatchers.IO){
val item = Detail(0, set = list.size+1)
repository.add(item)
// If use plus(), a new List is returned.
// Therefore, the change is notified to the Observer by postValue of the new list added.
_items.postValue(list.plus(item))
}
}
fun deleteDetail() {
// Delete the last set and return a new list to postValue to notify the Observer of the change.
_items.postValue(list.dropLast(1))
}
}
DiffUtil
class DetailDiffCallback : DiffUtil.ItemCallback<Detail>() {
override fun areItemsTheSame(
oldItem: Detail,
newItem: Detail
): Boolean {
return (oldItem.id == newItem.id)
}
override fun areContentsTheSame(
oldItem: Detail,
newItem: Detail
): Boolean {
return oldItem == newItem
}
}
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
vm.items.observe(viewLifecycleOwner) { newList ->
adapter.submitList(newList)
}
}
Detail list items were set as companion objects because it was determined that it was not necessary to have a unit property individually.
This is the root of the problem. If you want DiffUtil to be able to "see" these changes, you will have to move this information out of the companion object.
DiffUtil works by taking in two instances of your class and doing work (the areItemsTheSame() and areContentsTheSame() methods) to see if anything has changed. Since this information is part of the companion object, it will always be identical for all instances, which means there's no way for DiffUtil to detect a change, even if one has happened.
Like Ben P says, when you change the value in the companion object, that affects the entire class (since they all share that object). It's not included in the generated equals() code for the data class - because why would it need to be? Every instance shares the same value, it's not part of the state!
Even if it were, when you compare oldList and newList, oldList has still been "updated" with the new unit value, because it's in that shared companion object. If you want oldList and newList to be able to have different values for unit, they need to be per-instance properties.
But the way you're doing things here, the unit doesn't even seem to be part of the data, right? It's not like you're storing a value and a unit of measure, and doing conversions when the displayed unit type changes. It looks like this is just a general display option, that just applies to how the data is displayed.
So in that case, why not just call notifyDataSetChanged() on the adapter, or something similar? Force a redraw, let it show the new unit type, that's it. I'm not sure if you need to do anything special if you're using DiffUtil, but that's what I'd look into.
(I feel like storing the unit type in the data would be a way better approach, seems important to what the stored number actually means, but for what you're doing right now, a display refresh should be enough)

How to proper way of using Diff utils when updating view in adapter kotlin

Hey I am new in DiffUtil in adpater. I read some articles from stack overflow, google docs and some articles. I am trying to understand callback of DiffUtil areItemsTheSame and areContentsTheSame but, I am not clear what that means. I am adding some code, please have a look. If I am doing wrong please guide me.
GroupKey
data class GroupKey(
val type: EnumType,
val sender: Sender? = null,
val close: String? = null
)
EnumType
enum class EnumType {
A,
B
}
Sender
data class Sender(
val company: RoleType? = null,
val id: String? = null
)
RoleType
data class RoleType(
val name : String?= null
val id: String? = null
)
Group
data class Group(
val key: GroupKey,
val value: MutableList<Item?>
)
I am passing my list to adapter which is a Group mutableList
var messageGroupList: MutableList<Group>? = null
..
val adapter = MainAdapter()
binding.recylerview.adapter = adapter
adapter.submitList(groupList)
Using DiffUtil in adapter
MainAdapter.kt
class MainAdapter :ListAdapter<Group, RecyclerView.ViewHolder>(COMPARATOR) {
companion object {
private val COMPARATOR = 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))
}
}
}
.....
}
1. Here do I need to compare key other property like type, sender etc. also inside this DiffUtil.ItemCallback.
2. when to use == or === and what about equals()
3. If we compare int, boolean or String we use == or something else ?
Inside this adapter I am calling another Recyclerview with passing list of Item inside that adapter.
Item
data class Item(
val text: String? = null,
var isRead: Boolean? = null,
val sender: Sender? = null,
val id: Int? = null
)
NestedRecyclerView.kt
class NestedRecyclerView : ListAdapter<Item, IncomingMessagesViewHolder>(COMPARATOR) {
companion object {
private val COMPARATOR = 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.isRead == oldItem.isRead) &&
(oldItem.sender == newItem.sender) &&
(oldItem.text == oldItem.text))
}
}
}
}
Again Same question Do I need to compare sender's other property here as well.
4. In areItemsTheSame do I need to compare id or just oldItem == newItem this?
5. How to proper way to update my adapter items. In normal reyclerview we use notifiyDataSetChanged. But in diffutil do I need to call again submitList function and it will take care of everything?
adapter.submitList(groupList)
Questions 1 and 4:
areItemsTheSame means that the two instances represent the same data item, even if some of the contents might be different. Suppose you had a list of contacts, and Jane's middle initial has been changed, but the row should still represent the same person Jane. There might be distinct instances of your model class, with some different values, but they are supposed to represent the same row.
So, usually you will compare only one thing between the old and new items that will be the same for each of them in this case. Usually, if you're getting data from a database or API, there will be some unique ID that represents a data point, and that's all you need to compare in areItemsTheSame. For example, oldItem.id == newItem.id.
areContentsTheSame means that if the two instances were each displayed in your list, they would look identical. So if you are using a data class, it is sufficient to use oldItem == newItem because a data class has an equals function that compares every property.
In your Item callback code, it looks like your areItemsTheSame is correct, but your areContentsTheSame is overly complex. Since Item is a data class, you only need to compare the two items directly.
override fun areContentsTheSame(oldItem: Item, newItem: Item) = oldItem == newItem
In your Group callback code, maybe you can compare the GroupKeys of the old and new items if that is a valid way to determine the items are the same. Since you are using only a direct == comparison, when items change partially you might have some visual defects like views disappearing and reappearing instead of simply having some of their text change.
Question 2
You should rarely ever have to use === in Kotlin. It does not only check if two items are equivalent, but it checks if the two items refer to the exact same instance in memory. It is not appropriate for DiffUtil.ItemCallback at all.
Question 3
== is the correct way to compare any two objects. In Kotlin, even primitives should be compared this way because they behave like objects.
Question 5
With ListAdapter, you should always use submitList instead of notifyDataSetChanged. notifyDataSetChanged would cause a pointless refresh of all the views and defeat the purpose of using ListAdapter and DiffUtil.

Update a row in recyclerview with ListAdapter

I am trying to perform update & delete operation in a recyclerview with ListAdapter. For this example I am using LiveData to get updates as soon as data is updated.
I don't know why list doesn't shows updated data, but when I see logs it shows correct data.
Code:
#AndroidEntryPoint
class DemoActivity : AppCompatActivity() {
var binding: ActivityDemoBinding? = null
private val demoAdapter = DemoAdapter()
private val demoViewModel: DemoViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDemoBinding.inflate(layoutInflater)
setContentView(binding?.root)
initData()
}
private fun initData() {
binding?.apply {
btnUpdate.setOnClickListener {
demoViewModel.updateData(pos = 2, newName = "This is updated data!")
}
btnDelete.setOnClickListener {
demoViewModel.deleteData(0)
}
rvData.apply {
layoutManager = LinearLayoutManager(this#DemoActivity)
adapter = demoAdapter
}
}
demoViewModel.demoLiveData.observe(this, {
it ?: return#observe
demoAdapter.submitList(it)
Log.d("TAG", "initData: $it")
})
}
}
activity_demo.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activities.DemoActivity">
<Button
android:id="#+id/btn_update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:text="Update Data" />
<Button
android:id="#+id/btn_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:text="Delete Data" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_data"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="#id/btn_update" />
</RelativeLayout>
DemoAdapter:
class DemoAdapter() : ListAdapter<DemoModel, DemoAdapter.DemoViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DemoViewHolder {
val binding =
ListItemDeleteBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return DemoViewHolder(binding)
}
override fun onBindViewHolder(holder: DemoViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
}
inner class DemoViewHolder(private val binding: ListItemDeleteBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(student: DemoModel) {
binding.apply {
txtData.text = student.name + " " + student.visible
if (student.visible) txtData.visible()
else txtData.inVisible()
}
}
}
class DiffCallback : DiffUtil.ItemCallback<DemoModel>() {
override fun areItemsTheSame(oldItem: DemoModel, newItem: DemoModel) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: DemoModel, newItem: DemoModel) =
(oldItem.id == newItem.id) &&
(oldItem.visible == newItem.visible) &&
(oldItem.name == newItem.name)
}
}
DemoViewModel:
class DemoViewModel : ViewModel() {
var demoListData = listOf(
DemoModel(1, "One", true),
DemoModel(2, "Two", true),
DemoModel(3, "Three", true),
DemoModel(4, "Four", true),
DemoModel(5, "Five", true),
DemoModel(6, "Six", true),
DemoModel(7, "Seven", true),
DemoModel(8, "Eight", true)
)
var demoLiveData = MutableLiveData(demoListData)
fun updateData(pos: Int, newName: String) {
val listData = demoLiveData.value?.toMutableList()!!
listData[pos].name = newName
demoLiveData.postValue(listData)
}
fun deleteData(pos: Int) {
val listData = demoLiveData.value?.toMutableList()!!
listData.removeAt(pos)
demoLiveData.postValue(listData)
}
}
Martin's Solution: https://github.com/Gryzor/TheSimplestRV
I suggest you:
Do yourself a favor and add a proper ViewModel/Sealed Class to encapsulate your state.
Initialize your adapter in the usual order:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDeleteBinding.inflate(layoutInflater)
setContentView(binding?.root)
binding.recyclerView.layoutManager = ... (tip: if you won't change the layout manager, I suggest you declare it in the XML directly, skipping this line here. E.g.: app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager")
binding.recyclerView.adapter = yourAdapter
//now observe data which will ultimately lead to `adapter.submitList(...)`
initData()
}
Make sure your DiffUtil.ItemCallback is properly comparing your models. You did old == new in Content, but that's not comparing the content, that's comparing the whole thing. It's the same in this case (I assume, but we haven't seen your Delete model class), but it's best to be explicit about it; the id is not the "content" theoretically speaking for the purposes of this callback thing.
delAdapter.submitList(it.toMutableList()) this is fine, but if you do it (and you do) before the adapter is set, and the LayoutManager is set (as you do), then it's likely possible that the ListAdapter is not magically recomputing it.
Update After Seeing More of Your Code
Let's look at your mutation code (one of the various):
fun updateData(pos: Int, newName: String) {
val listData = demoLiveData.value?.toMutableList()!!
listData[pos].name = newName
demoLiveData.postValue(listData)
}
I see various problems here.
You're grabbing the value from the LiveData. No-Go. LiveData is a value-holder, but I wouldn't "pull it from there" at any time, expect when I receive it via the observation. LiveData is not a repository, it's just holding the value and offering you "guarantees" that it will be managed in conjunction with your lifecycleOwner.
You then use toMutableList() and while this creates a new instance of the List (List<DemoModel> in your case), it does not create a deep copy of the references in the list. Meaning the items in the new (and old) list, are the same, pointing to the exact same spot in memory.
You then perform this operation listData[pos].name = newName in the "new list" but you're effectively modifying the old list as well (you can set a breakpoint there, and inspect the contents of all the lists involved and notice how the same item at pos is now changed to the newName everywhere.
If you want to see even more, put a breakpoint here:
demoViewModel.demoLiveData.observe(this, {
demoAdapter.submitList(it) <--> BREAKPOINT HERE
})
Also put a breakpoint in ListAdapter.java (the android class) in the submitList method:
public void submitList(#Nullable List<T> list) {
mDiffer.submitList(list); ---> BREAKPOINT HERE
}
And when stopped at the 1st breakpoint, observe the value of the list (it) and it's reference. (the first time the breakpoints hit, continue, since we want to observe the list AFTER you mutate the list and not on the "first creation").
Now press your button to change something (update the list) and the breakpoint(s) are going to be hit again, now the submitList call will have a list and it's gonna look like:
notice the Reference: it's (in my example) ArrayList#100073.
Now continue... (the debugger), it will stop again in the mDiffer.submitList(list) line of ListAdapter.
Let's compare.
For the record, this is what I do:
binding.updateButton.setOnClickListener {
viewModel.updateData(0, "Hello World " + 5)
}
So The item at position "0" should be called "Hello World 5" now.
This is already visible here in the debugger:
It's correctly changed in the list, but we're submitting to the adapter... let's see what the adapter has internally (before this is applied), let's jump to the next breakpoint in ListAdapter#submitList():
Notice something strange here?
The item at position 0, is already modified. How?!
Simple, the reference to that object DemoModel is the same. In my example: it's DemoModel#10078.
So how can you prevent this?
Never pass a mutable list to your adapter, always pass a copy (and immutable!)
your Live Data should have been:
var demoLiveData = MutableLiveData(demoList.toList()) //To List creates a new copy of the list, immutable.
This reinforces the concept of a Single Source of Truth. When you mutate data, you need to be sure you know what the scope of the mutation is. The reason why you saw no "change" is because by mutating the data behind the scenes of the adapter, by the time the DiffUtil (Which is async) was called and the change dispatched, the list was already mutated and the Diff Util computed zero changes, which meant the adapter had nothing else to do.
Changing an item in the list, does not (and will never) trigger an adapter to "notify the data was changed", since the adapter is "not observing" the list.
I hope this clarifies your confusion and the importance of not using mutable data all over the place.
Last but not least, I created a super simple project to exercise your problem and pushed it to https://github.com/Gryzor/TheSimplestRV (or if you prefer to see the viewModel alone).
Feel free to look at it (I used one of the default templates so the code is in a Fragment, but... irrelevant of course).
Good luck! :)
Why does NOTIFY DATA SET CHANGED WORK THEN?!
Well, when you do that, you FORCE the adapter to rebind every item, therefore it has to go through the list again (which is changed) and the change is reflected, at the expense of CPU, Battery, flickering, position lost, annoyance to the user(s), etc.
Internally, ListAdapter checks reference of the lists you submit. So you need to create a new list for each update so new one directs another reference different from previous list. Additionally, when you need to update an object in this list, you should create a new object otherwise diff util won't work.

ListAdapter Diff does not dispatch updates on same list instance, but neither on different list from LiveData

it is a known issue that ListAdapter (actually the AsyncListDiffer from its implementation) does not update the list if the new list only has modified items but has the same instance. The updates do not work on new instance list either if you use the same objects inside.
For all of this to work, you have to create a hard copy of the entire list and objects inside.
Easiest way to achieve this:
items.toMutableList().map { it.copy() }
But I am facing a rather weird issue. I have a parse function in my ViewModel that finally posts the items.toMutableList().map { it.copy() } to the LiveData and gets observes in the fragment. Even with the hard copy, DiffUtil does not work. If I move the hard copy inside the fragment, then it works.
To get this easier, if I do this:
IN VIEW MODEL:
[ ... ] parse stuff here
items.toMutableList().map { it.copy() }
restaurants.postValue(items)
IN FRAGMENT:
restaurants.observe(viewLifecycleOwner, Observer { items ->
adapter.submitList(items)
... then, it doesn't work. But if I do this:
IN VIEW MODEL:
[ ... ] parse stuff here
restaurants.postValue(items)
IN FRAGMENT:
restaurants.observe(viewLifecycleOwner, Observer { items ->
adapter.submitList(items.toMutableList().map { it.copy() })
... then it works.
Can anybody explain why this doesn't work?
In the mean time, I have opened an issue on the Google Issue Tracker because maybe they will fix the AsyncListDiffer not updating same instance lists or items. It defeats the purpose of the new adapter. The AsyncListDiffer SHOULD ALWAYS accept same instance lists or items, and fully update using the diff logic that the user customises in the adapter.
I made a quick sample using DiffUtil.Callback and ListAdapter<T, K> (so I called submitList(...) on the adapter), and had no issues.
Then I modified the adapter to be a normal RecyclerView.Adapter and constructed an AsyncDiffUtil inside of it (using the same DiffUtil.Callback from above).
The architecture is:
Activity -> Fragment (contains RecyclerView).
Adapter
ViewModel
"Fake Repository" that simply holds a val source: MutableList<Thing> = mutableListOf()
Model
I've created a Thing object: data class Thing(val name: String = "", val age: Int = 0).
For readability I added typealias Things = List<Thing> (less typing). ;)
Repository
It's fake in the sense that items are created like:
private fun makeThings(total: Int = 20): List<Thing> {
val things: MutableList<Thing> = mutableListOf()
for (i in 1..total) {
things.add(Thing("Name: $i", age = i + 18))
}
return things
}
But the "source" is a mutableList of (the typealias).
The other thing the repo can do is "simulate" a modification on a random item. I simply create a new data class instance, since it's obviously all immutable data types (as they should be). Remember this is just simulating a real change that may have come from an API or DB.
fun modifyItemAt(pos: Int = 0) {
if (source.isEmpty() || source.size <= pos) return
val thing = source[pos]
val newAge = thing.age + 1
val newThing = Thing("Name: $newAge", newAge)
source.removeAt(pos)
source.add(pos, newThing)
}
ViewModel
Nothing fancy here, it talks and holds the reference to the ThingsRepository, and exposes a LiveData:
private val _state = MutableLiveData<ThingsState>(ThingsState.Empty)
val state: LiveData<ThingsState> = _state
And the "state" is:
sealed class ThingsState {
object Empty : ThingsState()
object Loading : ThingsState()
data class Loaded(val things: Things) : ThingsState()
}
The viewModel has two public methods (Aside from the val state):
fun fetchData() {
viewModelScope.launch(Dispatchers.IO) {
_state.postValue(ThingsState.Loaded(repository.fetchAllTheThings()))
}
}
fun modifyData(atPosition: Int) {
repository.modifyItemAt(atPosition)
fetchData()
}
Nothing special, just a way to modify a random item by position (remember this is just a quick hack to test it).
So FetchData, launches the async code in IO to "fetch" (in reality, if the list is there, the cached list is returned, only the 1st time the data is "made" in the repo).
Modify data is simpler, calls modify on the repo and fetch data to post the new value.
Adapter
Lots of boilerplate... but as discussed, it's just an Adapter:
class ThingAdapter(private val itemClickCallback: ThingClickCallback) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
The ThingClickCallback is just:
interface ThingClickCallback {
fun onThingClicked(atPosition: Int)
}
This Adapter now has an AsyncDiffer...
private val differ = AsyncListDiffer(this, DiffUtilCallback())
this in this context is the actual adapter (needed by the differ) and DiffUtilCallback is just a DiffUtil.Callback implementation:
internal class DiffUtilCallback : DiffUtil.ItemCallback<Thing>() {
override fun areItemsTheSame(oldItem: Thing, newItem: Thing): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: Thing, newItem: Thing): Boolean {
return oldItem.age == newItem.age && oldItem.name == oldItem.name
}
nothing special here.
The only special methods in the adapter (aside from onCreateViewHolder and onBindViewHolder) are these:
fun submitList(list: Things) {
differ.submitList(list)
}
override fun getItemCount(): Int = differ.currentList.size
private fun getItem(position: Int) = differ.currentList[position]
So we ask the differ to do these for us and expose the public method submitList to emulate a listAdapter#submitList(...), except we delegate to the differ.
Because you may be wondering, here's the ViewHolder:
internal class ViewHolder(itemView: View, private val callback: ThingClickCallback) :
RecyclerView.ViewHolder(itemView) {
private val title: TextView = itemView.findViewById(R.id.thingName)
private val age: TextView = itemView.findViewById(R.id.thingAge)
fun bind(data: Thing) {
title.text = data.name
age.text = data.age.toString()
itemView.setOnClickListener { callback.onThingClicked(adapterPosition) }
}
}
Don't be too harsh, I know i passed the click listener directly, I only had about 1 hour to do all this, but nothing special, the layout it's just two text views (age and name) and we set the whole row clickable to pass the position to the callback. Nothing special here either.
Last but not least, the Fragment.
Fragment
class ThingListFragment : Fragment() {
private lateinit var viewModel: ThingsViewModel
private var binding: ThingsListFragmentBinding? = null
private val adapter = ThingAdapter(object : ThingClickCallback {
override fun onThingClicked(atPosition: Int) {
viewModel.modifyData(atPosition)
}
})
...
It has 3 member variables. The ViewModel, the Binding (I used ViewBinding why not it's just 1 liner in gradle), and the Adapter (which takes the Click listener in the ctor for convenience).
In this impl., I simply call the viewmodel with "modify item at position (X)" where X = the position of the item clicked in the adapter. (I know this could be better abstracted but this is irrelevant here).
there's only two other implemented methods in this fragment...
onDestroy:
override fun onDestroy() {
super.onDestroy()
binding = null
}
(I wonder if Google will ever accept their mistake with Fragment's lifecycle that we still have to care for this).
Anyway, the other is unsurprisingly, onCreateView.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.things_list_fragment, container, false)
binding = ThingsListFragmentBinding.bind(root)
viewModel = ViewModelProvider(this).get(ThingsViewModel::class.java)
viewModel.state.observe(viewLifecycleOwner) { state ->
when (state) {
is ThingsState.Empty -> adapter.submitList(emptyList())
is ThingsState.Loaded -> adapter.submitList(state.things)
is ThingsState.Loading -> doNothing // Show Loading? :)
}
}
binding?.thingsRecyclerView?.adapter = adapter
viewModel.fetchData()
return root
}
Bind the thing (root/binding), get the viewModel, observe the "state", set the adapter in the recyclerView, and call the viewModel to start fetching data.
That's all.
How does it work then?
The app starts, the fragment is created, subscribes to the VM state LiveData, and triggers the Fetch of data.
The ViewModel calls the repo, which is empty (new), so makeItems is called the list now has items and cached in the repo's "source" list. The viewModel receives this list asynchronously (in a coroutine) and posts the LiveData state.
The fragment receives the state and posts (submit) to the Adapter to finally show something.
When you "click" on an Item, ViewHolder (which has a click listener) triggers the "call back" towards the fragment which receives a position, this is then passed onto the Viewmodel and here the data is mutated in the Repo, which again, pushes the same list, but with a different reference on the clicked item that was modified. This causes the ViewModel to push a new LIveData state with the same list reference as before, towards the fragment, which -again- receives this, and does adapter.submitList(...).
The Adapter asynchronously calculates this and the UI updates.
It works, I can put all this in GitHub if you want to have fun, but my point is, while the concerns about the AsyncDiffer are valid (and may be or been true), this doesn't seem to be my (super limited) experience.
Are you using this differently?
When I tap on any row, the change is propagated from the Repository
UPDATE: forgot to include the doNothing function:
val doNothing: Unit
get() = Unit
I've used this for a while, I normally use it because it reads better than XXX -> {} to me. :)
While doing
items.toMutableList().map { it.copy() }
restaurants.postValue(items)
you are creating a new list but items remains the same. You have to store that new list into a variable or passing that operation directly as a param to postItem.

PagedListAdapter.submitList() Behaving Weird When Updating Existing Items

Little story of this topic : the app just updating clicked row's values with dialog when confirmed. Uses pagination scenario on room database.
When an item added or removed, the latest dataset is fetched and passed to submitList method, then all changes are seen and worked well.
The problem starts there, if an existing item updated, again the latest dataset is fetched properly and passed to submitList, but this time changes didn't seem.
When i debug the DIFF_CALLBACK and caught my item in areItemsTheSame, the newHistory and oldHistory values are same! (How!)
There could be any bug in submitList method ?
Room v. : 2.1.0-alpha02
Paging v. : 2.1.0-beta01
After initializing, observe fetches list from room and passes to mHistoryAdapter.submitList(it). Then if i update an item, observe gets triggered again(and i'm seeing updated value in param it) and passes to submitList.
Unfortunately, adapter wont change...
mResolvedAddressViewModel = ViewModelProviders.of(this).get(ResolvedAddressViewModel::class.java)
mResolvedAddressViewModel.getAddresses(false).observe(this, Observer {
mHistoryAdapter.submitList(it)
})
All the parts
Model
#Parcelize
#Entity
data class ResolvedAddress(
#PrimaryKey var id: String = UUID.randomUUID().toString(),
var requestedLat: Double = 0.0,
var requestedLon: Double = 0.0,
var requestedAddress: String = "",
var lat: Double,
var lon: Double,
var address: String,
var country: String,
var countryCode: String,
var city: String,
var alias: String? = null,
var favorite: Boolean = false,
var provider: String? = null,
var lastUseDate: Long = 0L) : Parcelable
Adapter
class HistoryAdapter(var context: Context)
: PagedListAdapter<ResolvedAddress, HistoryItemHolder>(DIFF_CALLBACK) {
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ResolvedAddress>() {
override fun areItemsTheSame(
oldHistory: ResolvedAddress, newHistory: ResolvedAddress): Boolean {
return oldHistory.id == newHistory.id
}
override fun areContentsTheSame(
oldHistory: ResolvedAddress, newHistory: ResolvedAddress): Boolean {
return oldHistory == newHistory
}
}
}
}
Fragment
class HistoryFragment : Fragment() {
private lateinit var mHistoryAdapter: HistoryAdapter
private lateinit var mResolvedAddressViewModel: ResolvedAddressViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_history, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerViewHistory.setHasFixedSize(true)
recyclerViewHistory.layoutManager = LinearLayoutManager(activity)
recyclerViewHistory.itemAnimator = DefaultItemAnimator()
mHistoryAdapter = HistoryAdapter(context!!)
recyclerViewHistory.adapter = mHistoryAdapter
mResolvedAddressViewModel = ViewModelProviders.of(this)
.get(ResolvedAddressViewModel::class.java)
mResolvedAddressViewModel.getAddresses(false).observe(this, Observer {
mHistoryAdapter.submitList(it)
})
}
}
There's a couple things missing from the question that could help provide a more detailed answer.
Ex. What does your RecyclerView.Adapter look like? Does it extend PagedListAdapter?
What does your model class look like? Is it a Kotlin data class?
For the sake of providing an answer, let's assume those unknowns are what we expect.
If I understand the question, it seems like you're just updating an item and not removing or adding any items.
Therefore, the DiffUtil.ItemCallback's areItemsTheSame will always return true, because the old list and new list has not been modified in terms of their size.
Meaning, if you've updated an item, you've probably updated it's contents and not removed it from the list.
Therefore, areItemsTheSame will return true, because their ids are still the same.
It's more likely that the second method, areContentsTheSame will return false since you've updated the item's content.
If your model class, ResolvedAddress, is a Kotlin data class, then the method areContentsTheSame should return false when comparing the item that was updated from the old list and the new list. This should trigger the onBindViewHolder method in your adapter at this point for you to rebind that item with the updated data.
If that model is not a Kotlin data class, than you must make sure the class implements the compareTo method. If not, you are comparing the object's memory address vs the actual contents of the object. If that is the case, the method areContentsTheSame will always return true, since the object's memory address has not changed.
These are some debugging tips, as it is difficult to provide a clearer answer without more knowledge about how the code has been implemented.
I was having a similar issue but managed to fix it by updating the existing item with a new object rather than directly updating the existing item, as suggested by this answer here:
https://stackoverflow.com/a/54505078/10923311
The issue is with how submitList processes changes. If you are passing a reference to the same list, it will not show updates as it determines it is the same datasource. In Kotlin if you want to update the sourceList and pass it back to submitList, you can do so as follows:
submitList(originalList.toList().toMutableList().let {
it[index] = it[index].copy(property = newvalue) // To update a property on an item
it.add(newItem) // To add a new item
it.removeAt[index] // To remove an item
// and so on....
it
})

Categories

Resources