【问题标题】:Diff util not updating items UI in recycler viewDiffutil 不更新 recyclerview 中的项目 UI
【发布时间】:2021-10-07 05:32:26
【问题描述】:

我正在使用带有回收器视图的列表适配器,现在数据类包含一个变量 isChecked,这用于指示用户是否选择了此变量,代码正在更新列表,因为我可以看到日志(我为了测试目的而放置的)返回当前列表总是在用户单击一个项目时更新,但由于某种原因,UI 中的更改(基于 isChecked 变量)仅在滚动回收器视图或单击其他项目时反映.我放了一个 notifyDataSetChanged 来查看它是否强制列表更新并查看更新的视图是否正确并且它是否有效,但这会破坏使用 diff util 的整个目的。我的包装数据类中有一个嵌套列表,它在 diff util 中进行比较,如下所示

private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<MainDataClass>() {
            override fun areItemsTheSame(
                oldItem: MainDataClass,
                newItem: MainDataClass
            ): Boolean {
                if (oldItem is MainDataClass.Item && newItem is MainDataClass.Item) {
                    return oldItem.data.id == newItem.data.id
                } else if (oldItem is MainDataClass.List && newItem is MainDataClass.List) {
                    return oldItem.list == newItem.list
                } else return false
            }

            override fun areContentsTheSame(
                oldItem: MainDataClass,
                newItem: MainDataClass
            ): Boolean {
                if (oldItem is MainDataClass.Item && newItem is MainDataClass.Item) {
                    return oldItem.data == newItem.data
                } else if (oldItem is MainDataClass.List && newItem is MainDataClass.List) {
                    return oldItem.list == newItem.list
                } else return false
            }
        }

MainDataClass.List 包含上述特定项目的列表。

public class Item{

    private Integer count;
    private Integer id;
    private String icon_img;
    private String name;
    private String cover_img;
    private String group_name;
    private Integer parent_id;
    private Integer status;
    private boolean checked = false;
    private Integer whatToVisible;

    public Item(Integer count, Integer id, String icon_img, String cover_img, String group_name, Integer parent_id) {
        this.count = count;
        this.id = id;
        this.icon_img = icon_img;
        this.cover_img = cover_img;
        this.group_name = group_name;
        this.parent_id = parent_id;
        this.checked = false;
    }

    public Item(Integer id, String icon_img, String group_name, Integer parent_id, boolean checked) {
        this.id = id;
        this.icon_img = icon_img;
        this.name = name;
        this.group_name = group_name;
        this.parent_id = parent_id;
        this.checked = checked;
    }

    public Item(Integer id,Integer parent_id) {
        this.id = id;
        this.parent_id = parent_id;
    }

    public static Item objectExample() {
        return new TrendingGroupsResponse(-2, -2);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isChecked() {
        return checked;
    }

    public void setChecked(boolean checked) {
        this.checked = checked;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public Integer getCount() {
        return count;
    }

    public String getIcon_img() {
        return icon_img;
    }

    public void setIcon_img(String icon_img) {
        this.icon_img = icon_img;
    }

    public String getCover_img() {
        return cover_img;
    }

    @Override
    public int hashCode() {
        return this.id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Item)) return false;
        Item that = (Item) o;
        return checked == that.checked && Objects.equals(count, that.count) && Objects.equals(id, that.id) && Objects.equals(icon_img, that.icon_img) && Objects.equals(name, that.name) && Objects.equals(cover_img, that.cover_img) && Objects.equals(group_name, that.group_name) && Objects.equals(parent_id, that.parent_id) && Objects.equals(status, that.status) && Objects.equals(whatToVisible, that.whatToVisible);
    }

    @Override
    public String toString() {
        return "id: " + this.id;
    }

    public void setCover_img(String cover_img) {
        this.cover_img = cover_img;
    }

    public String getGroup_name() {
        if (name != null){
            group_name = name;
        }
        return group_name;
    }

    public void setGroup_name(String group_name) {
        this.group_name = group_name;
    }

    public Integer getParent_id() {
        return parent_id;
    }

    public void setParent_id(Integer parent_id) {
        this.parent_id = parent_id;
    }

    public void setCount(Integer count) {
        this.count = count;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getIconImg() {
        return icon_img;
    }

    public void setIconImg(String icon_img) {
        this.cover_img = icon_img;
    }

    public String getCoverImg() {
        return cover_img;
    }

    public void setCoverImg(String cover_img) {
        this.cover_img = cover_img;
    }

    public String getGroupName() {
        if (name != null) {
            group_name = name;
        }
        return group_name;
    }

    public void setGroupName(String group_name) {
        this.group_name = group_name;
    }

    public Integer getWhatToVisible() {
        return whatToVisible;
    }

    public void setWhatToVisible(Integer whatToVisible) {
        this.whatToVisible = whatToVisible;
    }

从主列表中单击主项时调用的方法

fun addOrRemoveSelectedItemsIfPresent(item: Item) {
        viewModelScope.launch {
            addOrRemoveItemsFromPopularItems(item.id.toString())
            updateAllItems(item)
            var itemList = _selectedItems.value
            if (itemList == null) itemList = ArrayList()
            itemList.forEach { item1: Item ->
                if (item1.id == item.id) {
                    itemList.remove(group)
                    selectedItemCount.set(selectedItemCount.get() - 1)
                    _selectedItems.value = itemList
                    return@launch
                }
            }
            itemList.add(response)
            selectedItemCount.set(selectedItemCount.get() + 1)
            _selectedItems.value = itemList
        }
    }

private fun addOrRemoveItemsFromPopularItems(id: String) {
        val popularItems = _popularItemsLiveData.value?.data
        popularItems?.forEach {
            if (it.id.toString() == id) {
                if (it.isChecked == null || it.isChecked == false) {
                    it.isChecked = true
                } else {
                    it.isChecked = false
                }
            }
        }
        _popularItemsLiveData.postValue(Success(popularItems))
    }

【问题讨论】:

  • 发布您的 MainDataClass 和子项目
  • @MartinMarconcini 我不能因为它不能透露,对不起,但我可以说它 MainDataClass 是一个密封类,包含一个项目的数据类和另一个具有列表的数据类项目
  • 我的意思是,您可以发布您的数据类、更改名称并显示您正在使用的数据类型的基本信息。如果您将两个列表与 kotlin 中的两个数据类进行比较,您将得到不同的结果,但如果您在这里比较不同的类型(正如您的 if 语句所证明的那样),我不确定 DiffUtil 会如何反应.或者,查看DiffUtil's Source Code ;)
  • 最后,你调试好了吗?您可以在该 diffUtil 中放置断点,模拟一个小型数据集并进行比较,或者甚至更好地编写一个单元测试来展示 which 案例失败。我的意思是this simple comparison 显示了它如何检测列表中的项目何时不同,因此您的问题出在其他地方。
  • @MartinMarconcini 一切都正确,diff util 正在按预期工作,问题出现在这里,有一个名为 isChecked 的布尔值,用于查看是否选择了该项目,默认情况下它是值为 false,并在更新列表的 viewModel 中更新,此列表的观察者再次提交列表,但是当我比较新旧项目的 isChecked 值时,两者都显示为 true(仅适用于所选组)跨度>

标签: android kotlin


【解决方案1】:

让我们在这里分解问题:

  1. 数据来自 API。
  2. 数据变成List&lt;Things&gt;(我们称它们为“事物”以保护它们的身份)。
  3. 您的 UI 显示 两个 列表,其中一个您已收到所有项目。我们称之为allList
  4. 您还希望根据用户从allList 中选择的项目,在另一个recyclerView 中显示此列表的子集。我们称之为selectedList。 (嘿,我没说我擅长命名)。

现在这里的关键是当您点击 allList 中的项目时会发生什么。如果该项目不存在,您希望将其添加到 selectedList,或者如果该项目已被选中,则将其从列表中删除

这就是你需要发生的事情。

我会这样做:

  1. 用户点击一个项目,我们将此事件与项目或其 ID 一起传递给 VM(视图模型)。 (我会传递整个项目,因为您已经拥有它,但这无关紧要)。
  2. VM 现在必须调用用例或类似于更新 选定项目列表 (selectedList)。 VM 并不真正关心该列表中的内容,这不是它的工作。这就是用例/委托。但是让我们暂时忽略抽象的好处,并假设 VM 有一种方法可以为您做到这一点:

fun updateSelectedList(selectedItem: Thing): List&lt;Thing&gt; //remember we call 'em "things".

此方法内部发生的事情尚不相关

然后,您通过 liveData 将此列表传递给您的 Fragment/UI,它将submitList(...)。这一切都有效。您还在某处更新了项目的isChecked 状态,因此allList 也可以更新其“视图状态”以选中或取消选中视图。

设想解决方案

在我们讨论潜在问题之前,假设您有一个完美的代码库,其中 updateSelectedList 返回了一个列表,您提交给您的 recyclerView 并且它工作正常(就像您现在声称它工作的 allList很好)。

在这个完美的场景中,DiffUtil 不会发现自己有任何麻烦,如果一个项目是新的(或消失的),它将能够计算差异并每次都在正确的位置呈现适当的视图,就像它经常一样可以。

问题

那么问题出在哪里呢?

您声称在 DiffUtil 调用时,isChecked 的值已更改,因此 diffUtil 未检测到差异。

这让我相信,以及我写这个冗长答案的原因,您传递给每个适配器的数据是相同的,所以当您将 selectedList 中的项目修改为 isSelected = true/false 时,您正在有效地修改 allList 包含的相同引用/对象。

为了更好地说明这一点,我制作了一个 Kotlin Playground 并举例说明了我的意思,它是可用的 here in play.kotlinlang.org

那么真正的怀疑问题是什么?

我相信您执行步骤的顺序会导致数据发生变异,以至于当 RecyclerView 需要计算更改时,它们已经发生了。

你能做什么?

我看到这样一个问题的方式是:

  1. 应该有一个关于列表及其状态的单一事实来源
  2. 提供给适配器的所有列表(全部和选定)都应该是不可变的副本。
  3. 当发生将改变任一列表的事件时(我假设它们都同时改变,因为当您选择或取消选择一个项目时,另一个列表必须反映更改), ViewModel 通过用例/repo 将执行转换并发出两个 new 不可变列表,其中的值已修改。

换句话说。

您选择(或取消选择)“所有列表”中的一个项目,您的 VM 评估需要完成的操作,并生成一个新的不可变列表,其中包含复制的项目。

您链接的答案建议使用toList() 这很好创建新列表 但这是浅拷贝,而不是深拷贝,所以项目仍然相同,有什么变化实际列表。含义:

val item1 = "A"
val item2 = "B"
val list1 = mutableList(item1, item2)
val list2 = list1.toList()

在此 ^^^ list2[0] 和 list2[1] 分别指向与 list1[0] 和 [1] 相同的对象。如果更改 item1,则在两个列表中都进行更改。

我怀疑这就是你正在发生的事情。

同样,不要toList。从最好是不可变对象构造一个新的不可变列表(所以使用data class 并使用 val,而不是 var)。

最好这样做:

val modifiedItem = oldItem.copy(isSelected = true)

我认为所有这些数据转换(修改两个列表和项目)应该在 ViewModel 中几乎同时发生(如果需要,可以委托给用例)。

我希望这能给你一个有用的方向。

记住:关注点分离是你的朋友。在您的完美世界中,适配器的工作是计算列表 A 和列表 B 之间的差异并渲染它们。仅此而已。

在选定列表中,所发生的只是项目出现和重新出现(基于它们是否被选中)。这个列表应该简单地由选定的项目组成(不需要是副本,只要你不以任何方式改变它们,只需显示它们)。

all 列表中,项目大多保持在相同的位置,但它们的内容会发生变化(以指示它们是否被选中)。再说一遍,这是一个不可变列表列表接收到的只读对象。

想想这个场景:

您的 all list 包含 2 项。

两者都没有选择。

您选择项目“A”(第一个)。如果您做对了,您收到的下一个列表是:

  1. 包含 1 个项目的选定列表。 (供选择)
  2. 一个包含 2 个项目的新“所有列表”,其中 item[0](第一个)是 isSelected = true。这将与 isSelected 为假的前一个列表进行比较。它应该可以工作。

我怎么知道这行得通?

几年前我不得不做一件非常相似的事情;)

【讨论】:

    猜你喜欢
    • 2020-06-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-17
    • 2017-09-13
    • 1970-01-01
    • 2019-08-11
    相关资源
    最近更新 更多