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

Posted

技术标签:

【中文标题】ListAdapter Diff 不会在同一个列表实例上分派更新,但也不会在与 LiveData 不同的列表上分派更新【英文标题】:ListAdapter Diff does not dispatch updates on same list instance, but neither on different list from LiveData 【发布时间】:2021-05-16 00:18:34 【问题描述】:

如果新列表仅具有修改的项目但具有相同的实例,则 ListAdapter(实际上是其实现的 AsyncListDiffer)不会更新列表是一个已知问题。如果您在内部使用相同的对象,更新也不适用于新实例列表。

要使所有这些工作,您必须创建整个列表和内部对象的硬拷贝。 实现这一目标的最简单方法:

items.toMutableList().map  it.copy() 

但我面临一个相当奇怪的问题。我的 ViewModel 中有一个解析函数,它最终将 items.toMutableList().map it.copy() 发布到 LiveData 并在片段中获取观察值。即使使用硬拷贝,DiffUtil 也不起作用。如果我将硬拷贝移动到片段中,那么它可以工作。

为了更容易,如果我这样做:

查看模型:

   [ ... ] parse stuff here

items.toMutableList().map  it.copy() 
restaurants.postValue(items)

在片段中:

 restaurants.observe(viewLifecycleOwner, Observer  items ->
     adapter.submitList(items)

...然后,它不起作用。但如果我这样做:

查看模型:

   [ ... ] parse stuff here

restaurants.postValue(items)

在片段中:

 restaurants.observe(viewLifecycleOwner, Observer  items ->
     adapter.submitList(items.toMutableList().map  it.copy() )

...然后就可以了。

谁能解释为什么这不起作用?

与此同时,我在 Google 问题跟踪器上打开了一个问题,因为他们可能会修复 AsyncListDiffer 不更新相同的实例列表或项目。它破坏了新适配器的目的。 AsyncListDiffer 应始终接受相同的实例列表或项目,并使用用户在适配器中自定义的差异逻辑进行完全更新。

【问题讨论】:

我想要以下来源:如果新列表仅包含修改过的项目但具有同一个实例;是这样吗? @MartinMarconcini 1. 这是一个堆栈溢出线程,它附加了许多其他报告该问题的线程。 ***.com/questions/57876118/… 2. 在第 256 行的 androidx.recyclerview.widget.AsyncListDiffer 中,您可以看到函数在开始时返回 newList == mList 和第 305 行 T oldItem = oldList.get(oldItemPosition); -> 对于同一个对象实例。 感谢您的参考。我已经很长时间没有真正使用过 AsyncDiff(通常只是直接执行 DiffUtil.Callback,因为对我来说最昂贵的部分通常是我这些天在 IO 上下文中的挂起函数中执行的“解析”和“转换”,但我明白你的意思。我认为 ycesar 的回答是准确的,你想通过深度复制旧项目来创建一个新列表。 【参考方案1】:

我使用DiffUtil.CallbackListAdapter<T, K> 做了一个快速示例(所以我在适配器上调用了 submitList(...)),没有任何问题。

然后我将适配器修改为普通的RecyclerView.Adapter,并在其中构造了一个 AsyncDiffUtil(使用上面相同的 DiffUtil.Callback)。

架构是:

    Activity -> Fragment(包含 RecyclerView)。 适配器 视图模型 “假存储库”仅包含 val source: MutableList<Thing> = mutableListOf()

型号

我创建了一个Thing 对象:data class Thing(val name: String = "", val age: Int = 0)

为了便于阅读,我添加了typealias Things = List<Thing>(少打字)。 ;)

存储库

从某种意义上说,它是假的,项目是这样创建的:

 private fun makeThings(total: Int = 20): List<Thing> 
        val things: MutableList<Thing> = mutableListOf()

        for (i in 1..total) 
            things.add(Thing("Name: $i", age = i + 18))
        

        return things
    

但“源”是(类型别名)的可变列表。

回购可以做的另一件事是“模拟”对随机项目的修改。我只是创建了一个新的数据类实例,因为它显然都是不可变的数据类型(它们应该是)。请记住,这只是模拟可能来自 API 或 DB 的真实更改。


    fun modifyItemAt(pos: Int = 0) 
        if (source.isEmpty() || source.size <= pos) return

        val thing = source[pos]
        val newAge = thing.age + 1
        val newThing = Thing("Name: $newAge", newAge)

        source.removeAt(pos)
        source.add(pos, newThing)
    

视图模型

这里没什么特别的,它会谈论并持有对 ThingsRepository 的引用,并公开一个 LiveData:

    private val _state = MutableLiveData<ThingsState>(ThingsState.Empty)
    val state: LiveData<ThingsState> = _state

而“状态”是:

sealed class ThingsState 
    object Empty : ThingsState()
    object Loading : ThingsState()
    data class Loaded(val things: Things) : ThingsState()

viewModel 有两个公共方法(除了val state):

    fun fetchData() 
        viewModelScope.launch(Dispatchers.IO) 
            _state.postValue(ThingsState.Loaded(repository.fetchAllTheThings()))
        
    

    fun modifyData(atPosition: Int) 
        repository.modifyItemAt(atPosition)
        fetchData()
    

没什么特别的,只是一种按位置修改随机项目的方法(记住这只是一个快速的测试方法)。

所以 FetchData,在 IO 中启动异步代码以“获取”(实际上,如果列表存在,则返回缓存列表,仅在 repo 中“生成”数据的第一次)。

修改数据更简单,在 repo 上调用 modify 并获取数据以发布新值。

适配器

大量样板文件......但正如所讨论的,它只是一个适配器:

class ThingAdapter(private val itemClickCallback: ThingClickCallback) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() 

ThingClickCallback 只是:

interface ThingClickCallback 
    fun onThingClicked(atPosition: Int)

这个适配器现在有一个 AsyncDiffer...

private val differ = AsyncListDiffer(this, DiffUtilCallback())

this 在这种情况下是实际的适配器(不同需要),DiffUtilCallback 只是 DiffUtil.Callback 的实现:

    internal class DiffUtilCallback : DiffUtil.ItemCallback<Thing>() 
        override fun areItemsTheSame(oldItem: Thing, newItem: Thing): Boolean 
            return oldItem.name == newItem.name
        

        override fun areContentsTheSame(oldItem: Thing, newItem: Thing): Boolean 
            return oldItem.age == newItem.age && oldItem.name == oldItem.name
        
    

这里没什么特别的。

适配器中唯一的特殊方法(除了 onCreateViewHolder 和 onBindViewHolder)是这些:

    fun submitList(list: Things) 
        differ.submitList(list)
    

    override fun getItemCount(): Int = differ.currentList.size

    private fun getItem(position: Int) = differ.currentList[position]

所以我们要求differ 为我们做这些,并公开公共方法submitList 来模拟listAdapter#submitList(...),除非我们委托给不同的人。

因为您可能想知道,这里是 ViewHolder:

    internal class ViewHolder(itemView: View, private val callback: ThingClickCallback) :
        RecyclerView.ViewHolder(itemView) 
        private val title: TextView = itemView.findViewById(R.id.thingName)
        private val age: TextView = itemView.findViewById(R.id.thingAge)

        fun bind(data: Thing) 
            title.text = data.name
            age.text = data.age.toString()
            itemView.setOnClickListener  callback.onThingClicked(adapterPosition) 
        
    

不要太苛刻,我知道我直接通过了点击监听器,我只有大约 1 小时来完成这一切,但没什么特别的,布局只是两个文本视图(年龄和姓名),我们设置整行可单击以将位置传递给回调。这里也没什么特别的。

最后但同样重要的是Fragment

片段

class ThingListFragment : Fragment() 
    private lateinit var viewModel: ThingsViewModel
    private var binding: ThingsListFragmentBinding? = null
    private val adapter = ThingAdapter(object : ThingClickCallback 
        override fun onThingClicked(atPosition: Int) 
            viewModel.modifyData(atPosition)
        
    )
...

它有 3 个成员变量。 ViewModel、Binding(我使用 ViewBinding 为什么不只是 gradle 中的 1 行)和 Adapter(为了方便起见,它在 ctor 中采用 Click 侦听器)。

在这个例子中,我只是用“修改位置 (X) 的项目”来调用视图模型,其中 X = 在适配器中单击的项目的位置。 (我知道这可以更好地抽象,但这在这里无关紧要)。

这个片段中只有另外两个实现的方法...

onDestroy:

    override fun onDestroy() 
        super.onDestroy()
        binding = null
    

(我想知道 Google 是否会接受他们在 Fragment 生命周期方面的错误,我们仍然需要关注这一点)。

反正另一个不出所料,onCreateView

 override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? 
        val root = inflater.inflate(R.layout.things_list_fragment, container, false)
        binding = ThingsListFragmentBinding.bind(root)

        viewModel = ViewModelProvider(this).get(ThingsViewModel::class.java)
        viewModel.state.observe(viewLifecycleOwner)  state ->
            when (state) 
                is ThingsState.Empty -> adapter.submitList(emptyList())
                is ThingsState.Loaded -> adapter.submitList(state.things)
                is ThingsState.Loading -> doNothing // Show Loading? :)
            
        

        binding?.thingsRecyclerView?.adapter = adapter
        viewModel.fetchData()

        return root
    

绑定东西(root/binding),获取viewModel,观察“状态”,在recyclerView中设置适配器,调用viewModel开始取数据。

就是这样。

那么它是如何工作的呢?

应用启动,创建fragment,订阅VMstateLiveData,触发Fetch数据。 ViewModel 调用 repo,它是空的(新的),所以 makeItems 被称为列表现在有项目并缓存在 repo 的“源”列表中。 viewModel 异步接收这个列表(在协程中)并发布 LiveData 状态。 片段接收状态并发布(提交)到适配器以最终显示一些东西。

当你“点击”一个项目时,ViewHolder(它有一个点击监听器)触发对接收位置的片段的“回调”,然后将其传递到 Viewmodel 和 这里的数据被改变 在 Repo 中,它再次推送相同的列表,但在已修改的单击项目上具有不同的引用。这会导致 ViewModel 将具有与之前相同的列表引用的新 LIveData 状态推送到片段,该片段再次接收到该状态,并执行 adapter.submitList(...)。

适配器异步计算此值并更新 UI。

它有效,如果您想玩得开心,我可以将所有这些都放在 GitHub 中,但我的观点是,虽然对 AsyncDiffer 的担忧是有效的(并且可能是或曾经是真的),但这似乎不是我的(超级有限)经验。

你有不同的用法吗?

当我点击任何一行时,更改将从存储库传播

更新:忘记包含doNothing 函数:


val doNothing: Unit
    get() = Unit

我已经使用了一段时间,我通常使用它,因为它对我来说比 XXX -&gt; 更好。 :)

【讨论】:

【参考方案2】:

在做的时候

items.toMutableList().map  it.copy() 
restaurants.postValue(items)

您正在创建一个新列表,但 items 保持不变。您必须将该新列表存储到一个变量中,或者将该操作作为参数直接传递给 postItem。

【讨论】:

以上是关于ListAdapter Diff 不会在同一个列表实例上分派更新,但也不会在与 LiveData 不同的列表上分派更新的主要内容,如果未能解决你的问题,请参考以下文章

如何将自定义 ListAdapter 设置为 appwidget 中的列表视图?

ListAdapter列表适配器

根据可用的水平空间有条件地在 ListAdapter 中显示项目

RecyclerView | 在 RecyclerView 中使用 ListAdapter

使用 ListAdapter 更新 recyclerview 中的一行

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