12

在使用 ListAdapter 时,我注意到在更新项目后DiffUtil.ItemCallback areContentsTheSame()方法总是返回 true。调试代码我发现旧项和新项完全一样(旧状态消失了)。因此,没有调用ListAdapter onBindViewHolder()方法来更新列表中的相应行(只有项目的顺序通过漂亮的动画进行了更改)。

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

ListAdapter 未更新 reyclerview 中的项目

编辑内容时 ListAdapter 不更新

DiffUtil.Callback 未按预期工作

Recyclerview + Listadapter 中的项目不会在更新时重绘

具有 DiffUtil.ItemCallback 的 ListAdapter 始终认为对象相同

向 RecyclerView ListAdapter 提交新列表时,差异检查始终为 areContentsTheSame() 返回 true

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

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

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

  1. notifyDataSetChanged()

即使它有效,但如果您决定首先使用 ListAdapter,肯定不是正确的方法。

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

没用。

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

4

5 回答 5

7

我不知道这是否是针对这种特定情况的解决方案,但根据描述,这听起来与我最近经历的完全一样。

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

我遇到的问题是由于我对更新列表并允许 DiffUtils 完成其应有的工作的理解。希望以下内容足够清楚:

我在做什么:

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

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

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

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

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


简而言之;

做这个:

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

不要这样做:

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

于 2020-07-31T14:51:48.210 回答
1

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

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()方法创建了一个复制对象,它对我有用。

于 2021-05-08T02:34:08.660 回答
0

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

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

这更好地解释了 java 通过值/引用传递: Java 是“按引用传递”还是“按值传递”?

我做了什么:

项目 newItem = new ItemBuilder().setId(oldItem.getId()).......

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

于 2020-08-01T20:07:31.983 回答
0

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

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

  • view 会将此点击委托给具有点击位置的 ViewModel。

  • ViewModel 将更新该位置的对象属性。就我而言,我有一个List<FeedPost> list. 每个FeedPost都有用户信息、连接按钮、计数等。所以如果用户点击 Like 按钮,我将在我的 ViewModel 中增加它的值list.get(clickedPos).incrementLikeByOne()

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

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

  • 这里要注意的一件事是你必须像这样使用它SingleLiveEventSingleLiveEvent<HashMap<Integer, String>> changeDetailsMap_SLE因为它是一次性操作,而不是 MutableLiveData,因为它本质上是粘性的。我正在使用片段,所以当片段从后台堆栈中弹出时,普通 LiveData 再次调度最近的值,因为当片段从后台堆栈中出来时它变得活跃。如果您不知道,您可以谷歌 SingleLiveEvent。您可以将其用于一次性操作。虽然,其中一位谷歌员工在他的中篇文章中提出了一种更好的方法,但实际上我发现这个 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。

于 2020-11-20T15:55:36.160 回答
0

我花了很多时间,直到我发现我遇到了与这篇文章中描述的相同的问题。我尝试了很多解决方案,但最后我意识到我在不知不觉中搞砸了。如果您遇到同样的问题,请检查您是否没有先更新 UI 并调用写入命令来更新存储库中的值。就我而言,我正在更新 LiveData 对象,然后在 Firebase 数据库上更新该值。这个问题是因为我的 UI 正在监听那个 LiveData 对象的变化,所以我的 Firebase 数据库观察器检测到了变化,调用了 ListAdapter 并且看起来没有任何变化。

为了解决这个问题,我删除了更新 LiveData 对象的行,我只调用了 Firebase 数据库来发送更改。

于 2020-02-11T22:55:40.413 回答