DiffUtil 不刷新观察者调用android kotlin中的视图

Posted

技术标签:

【中文标题】DiffUtil 不刷新观察者调用android kotlin中的视图【英文标题】:DiffUtil not refreshing view in Observer call android kotlin 【发布时间】:2021-12-11 08:54:11 【问题描述】:

嘿,我正在使用带有 ListAdapter 的 diff util。列表的更新有效,但我只能通过滚动列表来查看这些新值,即使不回收视图(滚动时),我也需要查看更新,就像 notifyItemChanged() 一样。我尝试了这个答案ListAdapter not updating item in RecyclerView 中的所有内容,仅对我有用的是 notifyItemChanged 或再次设置 adapter。我正在添加一些代码。请问有人知道如何解决这个问题吗?

数据和枚举类

data class GroupKey(
    val type: Type,
    val abc: Abc? = null,
    val closeAt: String? = null
)

data class Group(
    val key: GroupKey,
    val value: MutableList<Item?> = ArrayDeque()
)

enum class Type
  ONE,
  TWO


data class Abc(
    val qq: String? = null,
    val bb: String? = null,
    val rr: RType? = null,
    val id: String? = null
)

data class RType(
    val id: String? = null,
    val name: String? = null
)


data class Item(
    val text: String? = null,
    var abc: Abc? = null,
    val rr: rType? = null,
    val id: String? = null
)

viewmodel.kt

var list: MutableLiveData<MutableList<Group>?> = MutableLiveData(ArrayDeque())

 fun populateList()
  // logic to call api 
   list.postValue(data)
 


 fun addItemTop()
  // logic to add item on top
  list.postValue(data)
 

在视图模型内部我通过视图模型函数内部的 api 调用填充数据并将值返回到列表。还有另一个函数将项目插入列表顶部,这就是使用 ArrayDeque

的原因

现在我正在添加嵌套的 reyclerview diff util 回调。

FirstAdapter.kt

class FirstAdapter :
    ListAdapter<Group, RecyclerView.ViewHolder>(comp) 

    companion object 
        private val comp = object : DiffUtil.ItemCallback<Group>() 
            override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean 
                return oldItem == newItem
            

            override fun areContentsTheSame(oldItem: Group, newItem: Group): Boolean 
                return ((oldItem.value == newItem.value) && (oldItem.key == newItem.key))
            
        
    
 ......... more function of adapter

FirstViewHolder

val adapter = SecondAdapter()
binding.recyclerView.adapter = adapter
adapter.submitList(item.value)

SecondAdapter.kt

class SecondAdapter : ListAdapter<Item, OutgoingMessagesViewHolder>(comp) 

    companion object 
        private val comp = object : DiffUtil.ItemCallback<Item>() 
            override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean 
                return oldItem.id == newItem.id
            

            override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean 
                return ((oldItem.rr == newItem.rr) &&
                        (oldItem.text == oldItem.text) && (oldItem.abc == newItem.abc))
            
        
    
 ..... more function

Activity.kt

 viewModel.list.observe(this,  value ->
            submitList(value)
 )

private fun submitList(list: MutableList<Group>?) 
        adapter?.submitList(list)
 //       adapter?.notifyDataSetChanged()

我 100% 确定我的列表正在更新,并且当我的新列表添加时我的观察者正在调用。我通过调试视图进行调试。但问题是我只能通过滚动列表来查看这些新值,即使不回收视图(滚动时)我也需要查看更新,就像notifyItemChanged()

更新

viewmodel.kt

class viewModel : BaseViewModel()
    
 var list: MutableLiveData<MutableList<Group>?> = MutableLiveData()
//... more variables...


 fun fetchData(context: Context) 
        viewModelScope.launch 
            val response = retroitApiCall()
                response.handleResult(
                    onSuccess =  response ->
                                              
                    list.postValue(GroupData(response?.items, context))
                    ,
                    onError =  error ->
                       Log.e("error" ,"$error")
                    
                )
            
        
    

 internal fun GroupData(items: List<CItem>?, context: Context): MutableList<Group> 
        val result: MutableList<Group> = MutableList()

        items?.iterator()?.forEach  item ->
        // adding item in list by add function and then return list.
        return result
    


    private fun addItemOnTop(text: String) 
         list.value?.let  oldlist ->
          // logic to add items on top of oldlist variable
           if(top != null)
              oldlist.add(0,item)
           else
               val firstGroup = oldlist[0]
               firstGroup.value.add(item)
           
          list.postValue(oldlist)
         
    

我正在使用 sealed 类这样的东西,但不是这个Example。调用 api Retrofit Example 时类似于这些的东西。这两个链接我都给你举个例子。我在视图模型中使用的内容。

【问题讨论】:

【参考方案1】:

我不知道发生了什么,但我可以告诉你两件引起我注意的事情。

第一个适配器:

  override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean 
      return oldItem == newItem
  

您不是在比较项目是否相同,而是在比较项目及其内容是否相同。你没有像你在 second 适配器中那样的 ID 吗?

我可能会检查oldItem.key == newItem.key

提交列表

如the answer you linked 所示,submitList 有一个非常奇怪的逻辑,它比较实际列表的引用 是否相同,如果相同,则 什么都不做

在您的问题中,您没有显示列表的来源(通过似乎是 liveData 或 RXJava 的内容观察到),但是构造列表的 souce 是不可见的。

换句话说:

// P S E U D O   C O D E
val item1 = ...
val item2 = ...
val list1 = mutableListOf(item1, item2)

adapter.submitList(list1) // works fine
item1.xxx = ""
adapter.submitList(list1) // doesn't work well.

为什么?

不幸的是,submitList 的source code 告诉我们,如果对列表的引用相同,则不会计算差异。这实际上不在适配器上,而是在AsyncListDiffer 上,ListAdapter 在内部使用。 differ 负责触发计算。但如果列表引用相同,则不相同,它会默默地忽略它。

我怀疑您没有创建新列表。这种相当无证和沉默的行为弊大于利,因为通常情况下,开发人员并不期望复制提供给一个对象的列表,该对象的目的和承诺是提供“神奇地"(更重要的是,自动)计算它与前一个之间的差异。

我理解他们为什么这样做,但我会至少发出一个日志警告,表明您提供的是相同的列表。或者,如果你想避免污染已经被污染的 logCat,那么至少更加明确in its official documentation。

唯一的提示就是这个简单的短语:

当有新列表可用时,您可以使用submitList(List)

这里的关键是新列表这个词。所以不是包含新项目的相同列表,而只是一个新的List 引用(无论项目是否相同)。

你应该尝试什么?

我会先修改你的 submitList 方法:

private fun submitList(list: MutableList<Group>?) 
        adapter?.submitList(list.toMutableList())

对于 Java 用户来说:

adapter.submitList(new ArrayList(oldList));

更改是创建您收到的列表的副本list.ToMutableList()。这样AsyncListDiffer 的列表相等性检查将返回false 并且代码将继续。

更新/调试

很遗憾,我不知道您的代码发生了什么;我向您保证ListAdapter 有效,因为我自己每天都在使用它;如果你认为你发现了一个有问题的案例,我建议你创建一个小原型并将其发布到 github 或类似网站上,以便我们重现它。

我将从在关键区域使用调试/断点开始:

    视图模型;写下您“返回”的列表中的引用。 DiffUtil 方法,是否调用了 diffUtil? 您的 submitList() 方法,列表引用与您在 ViewModel 中的引用相同吗? 等

你需要更深入地挖掘,直到你发现谁没有做什么。

关于深拷贝和浅拷贝以及 Java 等等......

请记住,ListAdapter(通过 AsyncDiff)检查对列表的引用是否相同。换句话说,如果您有一个列表 val x = mutableListOf(...) 并将其提供给适配器,它将第一次工作。

如果您随后修改列表...

val x = mutableListOf(...)
adapter.submitList(x)

x.clear()
adapter.submitList(x)

这将无法正常工作,因为在适配器看来两个列表是相同的(它们实际上是同一个列表)。

列表是可变的这一事实无关紧要。 (我仍然对可变列表皱眉;为什么submitList 接受一个可变列表,如果你不能改变它并再次提交它,我不知道,但我不会批准这样的拉取请求)如果它会避免大多数问题他们只采用了一个不可变的列表,因此意味着如果您对其进行变异,则每次都必须提供一个新列表。总之……

正如我所说,复制列表很简单,在 Kotlin 或 Java 中都有多种变体:

val newListWithSameContents = list1.toList() 
List newListWithSameContents = ArrayList(list1);

现在如果list1 有一个项目...

list1.add("hello")

当您将 list1 复制到 newList... 对“Hello”(字符串)的引用相同。如果 String 是可变的(它不是,但假设它是),并且您以某种方式修改了该字符串...您将同时修改 both 字符串 或者更确切地说,相同的字符串,两个列表中都引用了

    data class Thing(var id: Int)

    val thing = Thing(1)
    val list1: MutableList<Thing> = mutableListOf(thing)
    val list2: MutableList<Thing> = list1.toMutableList()

    println(list1)
    println(list2)
// This prints
[Thing(id=1)]
[Thing(id=1)]

现在修改东西...

    thing.id = 2
    
    println(list1)
    println(list2)

正如所料,两个列表都指向同一个对象:

[Thing(id=2)]
[Thing(id=2)]

这是一个浅拷贝,因为项目没有被拷贝。它们仍然指向内存中相同的thing

ListAdapter/DiffUtil 不关心对象在这方面是否相同(取决于你如何实现你的 diffutil);但他们肯定关心列表是否相同。如上例所示。

我希望这能阐明 ListAdapter 调度更新所需的内容。如果没有这样做,请检查您是否有效地做正确的事情。

【讨论】:

感谢您回复我。我也尝试过这种方式。但它没有用,我更新了 viewmodel 代码。请看一看。我在 viewModel 中添加了一些我正在做的代码。再次感谢。 你检查了吗? 查看更新的答案。 我会在 github 中添加代码,并在准备好时标记你,谢谢 要跟踪两个引用:列表 (1) 和列表中的每个项目 (n)。 AsyncDiff 检查实际列表中的相等性,而不是项目。所以是的,你需要new ArrayList(oldList) 这给你一个新列表的新引用,它的项目与旧列表的引用完全相同;如果您检查oldListnew 列表的引用,您会发现它们是不同的。如果您在任一列表中修改 一个项目,它将在两个列表中都被修改。如果您从一个列表中删除一个项目,它不会从另一个列表中删除

以上是关于DiffUtil 不刷新观察者调用android kotlin中的视图的主要内容,如果未能解决你的问题,请参考以下文章

Android 高性能列表:RecyclerView + DiffUtil

Android DiffUtil

Android简易音乐重构MVVM Java版-使用DiffUtil解决recycleView整体数据刷新性能问题(二十二)

Android简易音乐重构MVVM Java版-使用DiffUtil解决recycleView整体数据刷新性能问题(二十二)

DiffUtil 和 registerAdapterDataObserver

Android详解7.0带来的新工具类:DiffUtil