使用 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 &amp;&amp; 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&lt;DemoModel&gt;),但它不会创建列表中引用的深层副本。含义新(和旧)列表中的项目是相同的,指向内存中完全相同的位置。 然后您在“新列表”中执行此操作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 中的一行的主要内容,如果未能解决你的问题,请参考以下文章

ListAdapter submitList 未更新

从android eclipse中的片段刷新或更新listadapter

ListAdapter Diff 不会在同一个列表实例上分派更新,但也不会在与 LiveData 不同的列表上分派更新

带有 DiffUtil (ListAdapter) 的 RecyclerView 会阻塞 UI 线程

带有DiffUtil的RecyclerView(ListAdapter)阻止UI线程

DiffUtil 重绘 ListAdapter Kotlin 中的所有项目