Reputation: 4494
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
Upvotes: 0
Views: 1105
Reputation: 27236
I suggest you:
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.
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.
lifecycleOwner
.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.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.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
.
your Live Data should have been:
var demoLiveData = MutableLiveData(demoList.toList()) //To List creates a new copy of the list, immutable.
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! :)
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.
Upvotes: 2
Reputation: 763
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.
Upvotes: 1