ListAdapter diff不在同一列表实例上调度更新,但也不在与LiveData不同的列表上调度更新

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

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

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

但我面临着一个相当奇怪的问题。我在我的ViewModel中有一个解析函数,它最终将items.toMuableList().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 Issue Tracker上打开了一个问题,因为他们可能会修复AsyncListDiffer不更新相同的实例列表或项目。它违背了新适配器的目的。AsyncListDiffer应始终接受相同的实例列表或项,并使用用户在适配器中自定义的比较逻辑完全更新。


解决方案

我使用DiffUtil.CallbackListAdapter<T, K>制作了一个快速样本(因此我称之为submitList(...)适配器上),并且没有问题。

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

架构为:

  1. 活动->;片段(包含循环视图)。
  2. 适配器
  3. 视图模型
  4. 仅包含val source: MutableList<Thing> = mutableListOf()
  5. 的虚假存储库(&Q)

型号

我已创建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
    }

但是";源";是(类型别名)的muableList。

repo可以做的另一件事是模拟对随机项目的修改。我只是创建了一个新的数据类实例,因为它显然都是不可变的数据类型(正如它们应该的那样)。请记住,这只是模拟可能来自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()
    }

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

So FetchData,将IO中的异步代码启动到";Fetch";(实际上,如果列表在那里,则只在数据在Repo中第一次生成";时才返回缓存的列表)。

修改数据更简单,调用Repo上的Modify和Fetch Data以发布新值。

适配器

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

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在此上下文中是实际的适配器(Different需要),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(...),除非我们委托Different。

因为您可能想知道,这里是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) }
        }
    }

不要太苛刻,我知道我直接传递了Click侦听器,我只有大约1个小时来完成所有这些操作,但没有什么特别的,布局它只有两个文本视图(年龄和姓名),我们设置了整行Clickable以将位置传递给回调。这里也没什么特别的。

最后但并非最不重要的是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、绑定(我使用的是ViewBinding,为什么不是Gradle中的1行代码)和Adapter(为方便起见,它在ctor中使用Click侦听器)。

在这个实现中,我只需使用&Modify Item at Position(X)&Quot;调用视图模型,其中X=适配器中单击的项目的位置。(我知道这可以更好地抽象,但这在这里无关紧要)。

此片段中只有两个其他实现的方法...

On Destroy:

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

(我想知道谷歌是否会接受他们在碎片生命周期上的错误,我们仍然需要关心这个错误)。

不管怎样,另一个并不令人意外,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
    }

绑定对象(根/绑定),获取viewModel,观察";状态";,在recumerView中设置适配器,然后调用viewModel开始获取数据。

仅此而已。

它是如何工作的?

启动应用程序,创建碎片,订阅VMstateLiveData,并触发数据获取。 ViewModel调用repo,而repo是空的(新的),因此将make Items称为List,现在列表中有项目并缓存在repo的源列表中。ViewModel以异步方式(在协程中)接收该列表并发布LiveData状态。 该片段接收状态并将其发送(提交)到Adapter以最终显示某些内容。

当您在一个项目上单击";时,ViewHolder(它有一个单击侦听器)触发对接收位置的片段的";回调,然后将其传递到ViewModel,并且此处数据在Repo中发生突变,这将再次推送相同的列表,但对已修改的已单击项目具有不同的引用。这会导致ViewModel将具有与以前相同的列表引用的新LIveData状态推送到片段,片段再次接收该列表,并执行Adapter.submitList(...)。

适配器将对此进行异步计算,并更新UI。

它很管用,如果你想找乐子,我可以把所有这些放在GitHub上,但我的观点是,尽管对AsyncDiffer的担忧是合理的(可能是真的,也可能是真的),但这似乎不是我(超级有限的)体验。

您是否以不同的方式使用它?

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

更新:忘记包括doNothing函数:


val doNothing: Unit
    get() = Unit

我使用这个有一段时间了,我通常使用它,因为它对我来说比XXX -> {}读起来更好。:)

相关文章