使用 ListAdapter 更新 recyclerview 中的一行
Update a row in recyclerview with ListAdapter
我正在尝试使用 ListAdapter
在 recyclerview 中执行更新和删除操作。对于此示例,我使用 LiveData
在数据更新后立即获取更新。
我不知道为什么列表不显示更新的数据,但是当我看到日志时它显示正确的数据。
代码:
@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>
演示适配器:
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)
}
}
演示视图模型:
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
我建议你:
- 帮自己一个忙,添加一个适当的 ViewModel/Sealed Class 来封装你的状态。
- 按照通常的顺序初始化您的适配器:
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()
}
确保您的 DiffUtil.ItemCallback 正确地比较了您的模型。您在内容中做了 old == new
,但这不是比较内容,而是比较整个内容。在这种情况下也是一样的(我假设,但我们还没有看到你的 Delete
模型 class),但最好明确说明; id
不是“内容”从理论上讲,为了这个回调的目的。
delAdapter.submitList(it.toMutableList())
这很好,但是如果您在设置适配器之前(并且您这样做了),并且设置了 LayoutManager(如您所做的那样),那么它很可能是可能的ListAdapter 并没有神奇地重新计算它。
看到更多你的代码后更新
让我们看看您的突变代码(其中一个):
fun updateData(pos: Int, newName: String) {
val listData = demoLiveData.value?.toMutableList()!!
listData[pos].name = newName
demoLiveData.postValue(listData)
}
我在这里看到了各种问题。
- 您正在从 LiveData 中获取值。不可以。 LiveData 是一个价值持有者,但我不会在任何时候“从那里拉出来”,希望通过观察收到它。 LiveData 不是存储库,它只是保存价值并向您“保证”它将与您的
lifecycleOwner
. 一起管理
- 然后您使用
toMutableList()
,虽然这会创建一个新的列表实例(List<DemoModel>
在您的情况下),它不会创建引用的深层副本列表。 表示新(和旧)列表中的项目相同,指向内存中完全相同的位置。
- 然后您在“新列表”中执行此操作
listData[pos].name = newName
但您实际上也在修改旧列表(您可以在那里设置一个断点,并检查所有涉及的列表的内容和请注意 pos
中的相同项目现在如何在所有地方更改为 newName
。
- 如果想看更多,请打断点:
demoViewModel.demoLiveData.observe(this, {
demoAdapter.submitList(it) <--> BREAKPOINT HERE
})
在submitList
方法中的ListAdapter.java
(androidclass)也打断点:
public void submitList(@Nullable List<T> list) {
mDiffer.submitList(list); ---> BREAKPOINT HERE
}
当在第一个断点处停止时,观察列表的值 (it
) 及其引用。 (第一次遇到断点,继续,因为我们想在你改变列表之后而不是在“第一次创建”时观察列表)。
现在按下你的按钮来改变一些东西(更新列表)并且断点将再次被击中,现在 submitList 调用将有一个列表,它看起来像:
注意参考:它是(在我的示例中)ArrayList@100073
。
现在继续...(调试器),它将再次停止在 ListAdapter 的 mDiffer.submitList(list)
行。
我们比较一下。
郑重声明,我就是这样做的:
binding.updateButton.setOnClickListener {
viewModel.updateData(0, "Hello World " + 5)
}
所以位置“0”的项目现在应该称为“Hello World 5”。
这已经在调试器中可见:
它在列表中已正确更改,但我们正在提交给适配器...让我们看看适配器内部有什么(在应用此之前),让我们跳到 ListAdapter#submitList() 中的下一个断点:
注意到这里有些奇怪吗?
位置 0 的项目,已经修改。如何?!
很简单,对那个对象的引用 DemoModel
是一样的。在我的示例中:它是 DemoModel@10078
.
那么如何防止这种情况呢?
- 永远不要将可变列表传递给您的适配器,始终传递一个副本(并且是不可变的!)
您的实时数据应该是:
var demoLiveData = MutableLiveData(demoList.toList()) //To List creates a new copy of the list, immutable.
- 这强化了单一事实来源的概念。当你改变数据时,你需要确保你知道改变的范围是什么。您看不到“变化”的原因是,通过在适配器的幕后 改变数据,在调用 DiffUtil(它是异步的)并分派变化时, list 已经发生变化,Diff Util 计算出零变化,这意味着适配器没有其他事情可做。
更改列表中的项目不会(也永远不会)触发适配器“通知数据已更改”,因为适配器“不观察”列表。
我希望这能澄清您的困惑以及不要到处使用可变数据的重要性。
最后但同样重要的是,我创建了一个超级简单的项目来练习您的问题并将其推送到 https://github.com/Gryzor/TheSimplestRV (or if you prefer to see the viewModel alone)。
随意看看(我使用了一个默认模板,所以代码在片段中,但是......当然不相关)。
祝你好运! :)
为什么 NOTIFY DATA SET CHANGED 起作用了?!
好吧,当你这样做时,你强制适配器重新绑定每个项目,因此它必须再次通过列表(已更改)并反映更改,但代价是 CPU , 电池, 闪烁, 丢失位置, 给用户带来烦恼等
ListAdapter 在内部检查您提交的列表的引用。因此,您需要为每个更新创建一个新列表,以便新列表指向另一个与之前列表不同的引用。此外,当您需要更新此列表中的对象时,您应该创建一个新对象,否则 diff util 将无法工作。
我正在尝试使用
ListAdapter
在 recyclerview 中执行更新和删除操作。对于此示例,我使用LiveData
在数据更新后立即获取更新。我不知道为什么列表不显示更新的数据,但是当我看到日志时它显示正确的数据。
代码:
@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>
演示适配器:
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)
}
}
演示视图模型:
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
我建议你:
- 帮自己一个忙,添加一个适当的 ViewModel/Sealed Class 来封装你的状态。
- 按照通常的顺序初始化您的适配器:
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()
}
确保您的 DiffUtil.ItemCallback 正确地比较了您的模型。您在内容中做了
old == new
,但这不是比较内容,而是比较整个内容。在这种情况下也是一样的(我假设,但我们还没有看到你的Delete
模型 class),但最好明确说明;id
不是“内容”从理论上讲,为了这个回调的目的。delAdapter.submitList(it.toMutableList())
这很好,但是如果您在设置适配器之前(并且您这样做了),并且设置了 LayoutManager(如您所做的那样),那么它很可能是可能的ListAdapter 并没有神奇地重新计算它。
看到更多你的代码后更新
让我们看看您的突变代码(其中一个):
fun updateData(pos: Int, newName: String) {
val listData = demoLiveData.value?.toMutableList()!!
listData[pos].name = newName
demoLiveData.postValue(listData)
}
我在这里看到了各种问题。
- 您正在从 LiveData 中获取值。不可以。 LiveData 是一个价值持有者,但我不会在任何时候“从那里拉出来”,希望通过观察收到它。 LiveData 不是存储库,它只是保存价值并向您“保证”它将与您的
lifecycleOwner
. 一起管理
- 然后您使用
toMutableList()
,虽然这会创建一个新的列表实例(List<DemoModel>
在您的情况下),它不会创建引用的深层副本列表。 表示新(和旧)列表中的项目相同,指向内存中完全相同的位置。 - 然后您在“新列表”中执行此操作
listData[pos].name = newName
但您实际上也在修改旧列表(您可以在那里设置一个断点,并检查所有涉及的列表的内容和请注意pos
中的相同项目现在如何在所有地方更改为newName
。 - 如果想看更多,请打断点:
demoViewModel.demoLiveData.observe(this, {
demoAdapter.submitList(it) <--> BREAKPOINT HERE
})
在submitList
方法中的ListAdapter.java
(androidclass)也打断点:
public void submitList(@Nullable List<T> list) {
mDiffer.submitList(list); ---> BREAKPOINT HERE
}
当在第一个断点处停止时,观察列表的值 (it
) 及其引用。 (第一次遇到断点,继续,因为我们想在你改变列表之后而不是在“第一次创建”时观察列表)。
现在按下你的按钮来改变一些东西(更新列表)并且断点将再次被击中,现在 submitList 调用将有一个列表,它看起来像:
注意参考:它是(在我的示例中)ArrayList@100073
。
现在继续...(调试器),它将再次停止在 ListAdapter 的 mDiffer.submitList(list)
行。
我们比较一下。
郑重声明,我就是这样做的:
binding.updateButton.setOnClickListener {
viewModel.updateData(0, "Hello World " + 5)
}
所以位置“0”的项目现在应该称为“Hello World 5”。
这已经在调试器中可见:
它在列表中已正确更改,但我们正在提交给适配器...让我们看看适配器内部有什么(在应用此之前),让我们跳到 ListAdapter#submitList() 中的下一个断点:
注意到这里有些奇怪吗?
位置 0 的项目,已经修改。如何?!
很简单,对那个对象的引用 DemoModel
是一样的。在我的示例中:它是 DemoModel@10078
.
那么如何防止这种情况呢?
- 永远不要将可变列表传递给您的适配器,始终传递一个副本(并且是不可变的!)
您的实时数据应该是:
var demoLiveData = MutableLiveData(demoList.toList()) //To List creates a new copy of the list, immutable.
- 这强化了单一事实来源的概念。当你改变数据时,你需要确保你知道改变的范围是什么。您看不到“变化”的原因是,通过在适配器的幕后 改变数据,在调用 DiffUtil(它是异步的)并分派变化时, list 已经发生变化,Diff Util 计算出零变化,这意味着适配器没有其他事情可做。 更改列表中的项目不会(也永远不会)触发适配器“通知数据已更改”,因为适配器“不观察”列表。
我希望这能澄清您的困惑以及不要到处使用可变数据的重要性。
最后但同样重要的是,我创建了一个超级简单的项目来练习您的问题并将其推送到 https://github.com/Gryzor/TheSimplestRV (or if you prefer to see the viewModel alone)。
随意看看(我使用了一个默认模板,所以代码在片段中,但是......当然不相关)。
祝你好运! :)
为什么 NOTIFY DATA SET CHANGED 起作用了?!
好吧,当你这样做时,你强制适配器重新绑定每个项目,因此它必须再次通过列表(已更改)并反映更改,但代价是 CPU , 电池, 闪烁, 丢失位置, 给用户带来烦恼等
ListAdapter 在内部检查您提交的列表的引用。因此,您需要为每个更新创建一个新列表,以便新列表指向另一个与之前列表不同的引用。此外,当您需要更新此列表中的对象时,您应该创建一个新对象,否则 diff util 将无法工作。