使用 ListAdapter 更新 recyclerview 中的一行
Posted
技术标签:
【中文标题】使用 ListAdapter 更新 recyclerview 中的一行【英文标题】:Update a row in recyclerview with ListAdapter 【发布时间】:2021-06-10 05:35:09 【问题描述】:我正在尝试使用ListAdapter
在回收站视图中执行更新和删除操作。对于这个例子,我使用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_
android:layout_
tools:context=".activities.DemoActivity">
<Button
android:id="@+id/btn_update"
android:layout_
android:layout_
android:layout_alignParentStart="true"
android:text="Update Data" />
<Button
android:id="@+id/btn_delete"
android:layout_
android:layout_
android:layout_alignParentEnd="true"
android:text="Delete Data" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_data"
android:layout_
android:layout_
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)
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)
马丁的解决方案:https://github.com/Gryzor/TheSimplestRV
【问题讨论】:
我认为这可以帮助你。 ***.com/questions/49726385/… 我很困惑,为什么您的活动中有 LiveData?那应该在 viewModel 中。areContentsTheSame
也很懒...我会做oldItem.something == newItem.something && oldItem.otherThing == newItem.otherThing
等...不仅仅是比较整个项目(这可能会导致意外的错误,具体取决于您的用例)
好的,我看到你说“为了简单起见,你没有使用 VM”。但是我在您的代码中没有看到...(我还没有喝咖啡)分配给 RecyclerView 的适配器在哪里?在底部......您的代码在“顺序”方面有点奇怪。您的“简单性”导致它更难遵循,因为不执行 ViewModel 等(实际上最多需要 10 分钟来设置),您违反了约定,所以答案是“我没有知道发生了什么”。
UNRELATED: lateinit var delAdapter: DeleteAdapter
不需要这样做,你可以这样做 val adapter = DeleteAdapter()
在你将它附加到 RV 之前它不会做任何事情(你应该在绑定东西之后做,但是它如果你有特定的理由可以等待,我不会)。
【参考方案1】:
我建议你:
-
帮自己一个忙,添加适当的 ViewModel/Sealed 类来封装您的状态。
按通常的顺序初始化您的适配器:
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
模型类),但最好是明确的; 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
(android类)中放一个断点:
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(这是异步的)并分派更改时,列表已经mutated 并且 Diff Util 计算出零变化,这意味着适配器无事可做。
更改列表中的项目不会(也永远不会)触发适配器“通知数据已更改”,因为适配器“未观察”列表。
我希望这能澄清您的困惑以及不要到处使用可变数据的重要性。
最后但同样重要的是,我创建了一个超级简单的项目来解决您的问题并将其推送到https://github.com/Gryzor/TheSimplestRV(或者如果您更喜欢see the viewModel alone)。
请随意查看(我使用了默认模板之一,因此代码位于片段中,但是...当然无关紧要)。
祝你好运! :)
为什么 NOTIFY DATA SET CHANGED 工作那么?!
好吧,当您这样做时,您会强制适配器重新绑定每个项目,因此它必须再次遍历列表(已更改)并反映更改,但代价是 CPU、电池、闪烁、位置丢失,给用户带来烦恼等。
【讨论】:
拜托了! :) 因为最终,这些都可能无关紧要,但更容易看出你的代码是否更有条理:) 你的方法行不通!看看我更新的问题。 我今天晚些时候再看,我在工作atm。 我看到各种问题,我会更新答案。 有没有办法直接更新一行。首先,我们必须删除并添加一个模型。【参考方案2】:在内部,ListAdapter 检查您提交的列表的引用。因此,您需要为每个更新创建一个新列表,以便新列表指向与先前列表不同的另一个引用。此外,当您需要更新此列表中的对象时,您应该创建一个新对象,否则 diff util 将不起作用。
【讨论】:
以上是关于使用 ListAdapter 更新 recyclerview 中的一行的主要内容,如果未能解决你的问题,请参考以下文章
从android eclipse中的片段刷新或更新listadapter
ListAdapter Diff 不会在同一个列表实例上分派更新,但也不会在与 LiveData 不同的列表上分派更新
带有 DiffUtil (ListAdapter) 的 RecyclerView 会阻塞 UI 线程