使用 ListAdapter 更新 recyclerview 中的一行

Update a row in recyclerview with ListAdapter

代码:

@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

我建议你:

  1. 帮自己一个忙,添加一个适当的 ViewModel/Sealed Class 来封装你的状态。
  2. 按照通常的顺序初始化您的适配器:
 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()
    }

  1. 确保您的 DiffUtil.ItemCallback 正确地比较了您的模型。您在内容中做了 old == new,但这不是比较内容,而是比较整个内容。在这种情况下也是一样的(我假设,但我们还没有看到你的 Delete 模型 class),但最好明确说明; id 不是“内容”从理论上讲,为了这个回调的目的

  2. delAdapter.submitList(it.toMutableList()) 这很好,但是如果您在设置适配器之前(并且您这样做了),并且设置了 LayoutManager(如您所做的那样),那么它很可能是可能的ListAdapter 并没有神奇地重新计算它。

看到更多你的代码后更新

让我们看看您的突变代码(其中一个):

    fun updateData(pos: Int, newName: String) {
        val listData = demoLiveData.value?.toMutableList()!!
        listData[pos].name = newName
        demoLiveData.postValue(listData)
    }

我在这里看到了各种问题。

  1. 您正在从 LiveData 中获取值。不可以。 LiveData 是一个价值持有者,但我不会在任何时候“从那里拉出来”,希望通过观察收到它。 LiveData 不是存储库,它只是保存价值并向您“保证”它将与您的 lifecycleOwner.
  2. 一起管理
  3. 然后您使用 toMutableList(),虽然这会创建一个新的列表实例(List<DemoModel> 在您的情况下),它不会创建引用的深层副本列表。 表示新(和旧)列表中的项目相同,指向内存中完全相同的位置。
  4. 然后您在“新列表”中执行此操作 listData[pos].name = newName 但您实际上也在修改旧列表(您可以在那里设置一个断点,并检查所有涉及的列表的内容和请注意 pos 中的相同项目现在如何在所有地方更改为 newName
  5. 如果想看更多,请打断点:
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.

那么如何防止这种情况呢?

  1. 永远不要将可变列表传递给您的适配器,始终传递一个副本(并且是不可变的!)

您的实时数据应该是:

var demoLiveData = MutableLiveData(demoList.toList()) //To List creates a new copy of the list, immutable. 
  1. 这强化了单一事实来源的概念。当你改变数据时,你需要确保你知道改变的范围是什么。您看不到“变化”的原因是,通过在适配器的幕后 改变数据,在调用 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 将无法工作。