DiffUtil ItemCallback areContentsTheSame()在ListAdapter上更新项目后始终返回true

时间:2019-09-10 18:03:38

标签: android android-recyclerview listadapter android-diffutils

在使用ListAdapter时,我注意到在更新项目之后, DiffUtil.ItemCallback areContentsTheSame()方法始终返回true。调试代码后,我意识到旧项目和新项目完全相同(旧状态消失了)。因此,没有调用 ListAdapter onBindViewHolder()方法来更新列表中的相应行(仅使用漂亮的动画更改了项目的顺序)。

检查有关Stackoverflow的其他问题似乎是许多开发人员面临的常见问题:

ListAdapter not updating item in reyclerview

ListAdapter not updating when editing content

DiffUtil.Callback not working as expected

Items in Recyclerview + Listadapter won't redraw on update

ListAdapter with DiffUtil.ItemCallback always considers objects the same

When submitting a new list to RecyclerView ListAdapter the diff check always returns true for areContentsTheSame()

  

但是以上所有答案(如果有)都没有提供正确的解决方案。

它对我有用的唯一方法是每次观察者发出新结果时都在ListAdapter上调用 notifyDataSetChanged()

但是,如果通过强制RecyclerView重绘其内容(它具有的所有项)而扔掉了所有可以得到的出色性能,那么使用ListAdapter和commitList()通知更改的全部意义是什么?显示到目前为止)?

  1. notifyDataSetChanged()

即使它可以工作,但是如果您决定首先使用ListAdapter,那么肯定不是正确的方法。

  1. viewModel.getObjects().observe(this, listOfObjects -> listAdapter.submitList(new ArrayList<>(listOfObjects)));

没用。

更新列表中的项目后,我希望看到UI上相应行的相应更改(不仅是排序顺序)符合预期。

5 个答案:

答案 0 :(得分:0)

我花了很多时间直到发现自己遇到了与本帖子中所述相同的问题。我尝试了许多解决方案,但最后我意识到我在不知情的情况下搞砸了。如果您遇到同样的问题,请检查是否不首先更新UI并调用write命令来更新存储库中的值。 就我而言,我正在更新LiveData对象,然后在Firebase数据库上更新该值。造成此问题的原因是,我的UI正在侦听该LiveData对象上的更改,因此我的Firebase数据库观察器检测到了更改,调用了ListAdapter,看起来好像没有任何更改。

为解决该问题,我删除了用于更新LiveData对象的行,而仅调用Firebase数据库发送更改。

答案 1 :(得分:0)

我不知道这是否是针对特定情况的解决方案,但是根据描述,这听起来就像我最近经历的事情。

我第一次使用的是新的Room + LiveData + ViewModel + DiffUtils集成,更新列表后,我在更新RecyclerView中的项目时遇到了同样的问题。

我遇到的问题是由于我了解更新列表并允许DiffUtils尽其应有的职责。希望以下内容足够清楚:

我在做什么:

  1. 用户使用对话框更新其项目
  2. RecyclerView中的列表项已更新为新信息
  3. 房间触发LiveData观察器,因为可能有 改变了
  4. RecyclerView DiffUtils尝试检查之间的任何差异 旧的适配器列表和新的适配器列表
  5. 未检测到新内容
  6. 未触发适配器更新其UI

我的错误是认为问题出在.5中,这使我花了半天的时间来回调试该问题。最终,我偶然发现了一个SO问题(目前无法找到),使我找到了问题的正确根源。这个问题确实存在于.2中-更新列表中的项目。

这是问题所在,因为甚至在DiffUtils有机会比较旧列表和新列表更改之前,我们都在用新更改更新CURRENT适配器列表。这意味着DiffUtils每次都将列表与已包含新列表所做更改的旧列表进行比较。

解决方案?不要更新列表项,因为它是一个对象,并且列表对象保留相同的引用,导致在使用/引用它的任何地方都更新同一实例。 而是克隆项目对象(就像在深层克隆中一样,我知道这可能很烦人),将用户对该克隆对象所做的更改应用到该对象,然后使用该克隆来更新Room数据库中的项目条目。 不要用克隆替换适配器列表项,而要保留原始列表,我们只想更新数据库中的信息,而不是列表中的信息,因为DiffUtils会解决这个问题。

我们在这里实际上要做的是为我们的房间数据库创建一个更新有效负载,然后将触发LiveData观察者将旧列表与新列表进行比较(包含更新的项目数据),从而在两者之间进行预期的更改检测列表。


简而言之;

执行此操作:

  • 深层克隆适配器列表项
  • 仅使用新信息更新克隆
  • 使用克隆信息更新数据库

不要这样做:

  • 直接更新适配器列表项
  • 使用列表项信息更新数据库

答案 2 :(得分:0)

我的声誉为0,所以我无法发表评论,但是@Shadow的回答对我有用。

我还使用Room + LiveData + ViewModel + DiffUtil并使用对话框编辑列表项的内容。尽管java是按值传递的,但是当我们传递对象的值时,实际上是传递了对其的引用。因此,当您在对话框中编辑同一对象的内容时,实际上是在更改列表项的相同引用,因此为什么LiveData进行onChange时DiffUtil无法计算差异。

这更好地解释了按值/引用传递Java: Is Java "pass-by-reference" or "pass-by-value"?

我做了什么:

Item newItem = new ItemBuilder()。setId(oldItem.getId()).......

然后更改newItem,然后从viewModel更新数据库

答案 3 :(得分:0)

我最近也面临这个问题。对于部分ViewHolder更改,我的方法是这样的:

  • 在适配器列表项中检测到单击时->通过回调将其委托给您的视图

  • 视图会将此次点击委派给具有点击位置的ViewModel。

  • ViewModel将更新该位置的对象属性。就我而言,我有一个List<FeedPost> list。每个FeedPost都有用户信息,连接按钮,计数等。因此,如果用户单击“喜欢”按钮,我将像list.get(clickedPos).incrementLikeByOne()一样在ViewModel中增加其值。

  • 现在,更新ViewModel中的列表也将反映适配器列表中的更改,因为适配器列表实质上包含相同的对象引用。当您执行add()addAll()时,并没有制作列表项目的深层副本。这就是DiffUtil无法检测到部分视图持有者更改的原因。

  • 我所做的是创建一个HashMap<Integer, Object> changeDetailsMap。该映射将包含其值已更改的数据,并且可以定位键,并在areContentsTheSame()中使用此映射返回true / false,以触发ViewHolder的部分更改。我之所以使用对象,是因为我可以传递我想要的任何东西(整数,字符串或我自己的POJO)的原因,但是您必须注意将其正确地投射在DiffUtil中。确保要放在此HashMap中的任何对象都有一个int字段(假设为int classType,以便可以将其强制转换为正确的Class)

  • 这里要注意的一件事是,您必须像这样使用SingleLiveEventSingleLiveEvent<HashMap<Integer, String>> changeDetailsMap_SLE,因为它是一次性操作,而不是MutableLiveData,因为它本质上是粘性的。我正在使用fragment,所以当fragment从backstack中弹出时,普通的LiveData会再次调度最近的值,因为当fragment从backstack中出来时,它会变为活动状态。如果您不知道,可以在Google SingleLiveEvent中进行搜索。您可以将其用于一次操作。虽然,在他的中篇文章中的一位Google员工提出了一种更好的方法,但是实际上我发现此SingleLiveEvent易于使用。

  • 按下此哈希图即可查看:changeDetailsMap_SLE.setValue(changeDetailsMap)。您可以像正常LiveData一样观察此SingleLiveEvent,并将其传递给适配器,然后将其分发给DiffUtil。我在适配器中执行此操作的方式就是这样

     public void updateConnectionStatus(Map<String, String>) {
         List<FeedAdapterModel> pseudoNewList = new ArrayList<>(adapterList); //adapterList is the list which you already have in your adapter class
         DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new FeedDiffUtil(adapterList, pseudoNewList, updatedConnectionStatusMap));
         adapterList.clear();
         adapterList.addAll(pseudoNewList);
         diffResult.dispatchUpdatesTo(this);
     }
    
  • 此外,在适配器中完成工作后,请不要忘记清除此哈希图。当您再次按下此SingleLiveEvent时,您不想获得之前为DiffUtil运行的旧职位。

在使用areItemsTheSame()方法而不是使用equals()这样的对象属性比较列表项(在oldList.get(oldItemPosition).getPostId() == newList.get(newItemPosition).getPostId()内部)的情况下,此方法也适用。在我的情况下,我有一个异构的adapterList,因此不能保证所有列表项都具有postID,所以我不能依赖于对象属性比较,因为它可以为null。

答案 4 :(得分:0)

几天前,我遇到了同样的问题。我试图像这样更新适配器内的对象:

binding.accept.setOnClickListener {
            existingEntity.taskStatus = "accept"
            listener.onItemClick(existingEntity)
        }

上面的代码正在更新数据库对象,但没有反映在我的 recyclerview 上。这是因为对象的相同引用。因此,当我更新适配器中的对象时,它会自动更新列表中的对象。所以我改变了我的代码如下:

binding.accept.setOnClickListener {
            val newEntity = existingEntity.copy()
            newEntity.taskStatus = "accept"
            listener.onItemClick(newEntity)
        }

所以我使用 Kotlin 数据类 copy() 方法创建了一个复制对象,它对我有用。