MENU

【译】使用 DiffUtil 方便地更新 RecyclerView

June 17, 2021 • 开发

现在可以和 notifyDataSetChanged() 说再见了···

在我们的日常开发中会大量使用 List。当用户滚动列表时需要更新列表的数据。为了实现我们通常会从服务器获取并更新接收到的数据。

如果更新过程中的出现了一些延迟,则会影响用户体验,因此我们希望尽快完成更新列表的操作,同时减少资源消耗。

当我们的列表中的内容改变时,我们会调用 notifyDataSetChanged() 来刷新数据,但开销很大。实际上我们可以使用多种方法替代 notifyDataSetChanged() 完成数据更新的操作。

由此引出 DiffUtil,Android 开发了这个实用工具类来处理 RecyclerView 的数据更新。

什么是 DiffUtil

从 24.2.0 开始,RecyclerView 支持库 v7 包提供了非常方便的实用工具类 DiffUtil。它可以找到两个列表之间的差异,并将更新后的列表作为输出提供。此类用于通知 RecyclerView 的 Adapter 进行数据更新。

它使用 Eugene W. Myers 的差分算法来计算最小数量的更新。

如何使用?

DiffUtil.Callback 是一个抽象类,作为回调类在 DiffUtil 计算两个列表间差异时来使用。该类有四个抽象方法以及一个非抽象方法,你必须继承并重载它的以下所有方法:

getOldListSize() 返回旧列表的大小

getNewListSize() 返回新列表的大小

areItemsTheSame(int oldItemPosition, int newItemPosition) 两个对象是否代表相同 Item。

areContentsTheSame(int oldItemPosition, int newItemPosition) 两个 item 是否含有相同数据,这个方法仅会在 areItemsTheSame() 方法返回 true 时被调用。

getChangePayload(int oldItemPosition, int newItemPosition) 如果 areItemTheSame() 返回 true 且 areContentsTheSame() 返回 false 时 DiffUtil 调用该方法来获取这次修改的开销。

以下是一个简单的 Employee 类,它使用 EmployeeRecyclerViewAdapterEmployeeDiffCallback 来对 Employee 进行排序。

public class Employee {
    public int id;
    public String name;
    public String role;
}

Diff.Callback 类的实现如下,你可以看到 getChangePayload() 并非一个抽象方法。

public class EmployeeDiffCallback extends DiffUtil.Callback {

    private final List<Employee> mOldEmployeeList;
    private final List<Employee> mNewEmployeeList;

    public EmployeeDiffCallback(List<Employee> oldEmployeeList, List<Employee> newEmployeeList) {
        this.mOldEmployeeList = oldEmployeeList;
        this.mNewEmployeeList = newEmployeeList;
    }

    @Override
    public int getOldListSize() {
        return mOldEmployeeList.size();
    }

    @Override
    public int getNewListSize() {
        return mNewEmployeeList.size();
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return mOldEmployeeList.get(oldItemPosition).getId() == mNewEmployeeList.get(
                newItemPosition).getId();
    }

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        final Employee oldEmployee = mOldEmployeeList.get(oldItemPosition);
        final Employee newEmployee = mNewEmployeeList.get(newItemPosition);

        return oldEmployee.getName().equals(newEmployee.getName());
    }

    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        // 如果要使用 ItemAnimator 则需要实现该方法
        return super.getChangePayload(oldItemPosition, newItemPosition);
    }
}

DiffUtil.Callback 的实现完成后,你需要按照如下方式更新在 RecyclerViewAdapter 中的列表

public class CustomRecyclerViewAdapter extends RecyclerView.Adapter<CustomRecyclerViewAdapter.ViewHolder> {

  ...
       public void updateEmployeeListItems(List<Employee> employees) {
        final EmployeeDiffCallback diffCallback = new EmployeeDiffCallback(this.mEmployees, employees);
        final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);

        this.mEmployees.clear();
        this.mEmployees.addAll(employees);
        diffResult.dispatchUpdatesTo(this);
    }
}

调用 dispatchUpdatesTo(RecyclerView.Adapter) 来分发更新后的列表。DiffResult 对象保存了 diff 运算后的结果,将修改分发给 Adapter,Adapter 会根据结果通知修改。

getChangePayload() 返回的结果通过 DiffResultnotifyItemRangeChanged(position, count, payload) 方法进行分发,并在后续调用 Adapter 的 onBindViewHolder(… List<Object> payloads) 方法。

@Override
public void onBindViewHolder(ProductViewHolder holder, int position, List<Object> payloads) {
// Handle the payload
}

DiffUtil 同样使用 RecyclerView.Adapter 的方法来通知 Adapter 更新数据集:

  • notifyItemMoved()
  • notifyItemRangeChanged()
  • notifyItemRangeInserted()
  • notifyItemRangeRemoved()

你可以在这里查看更多 RecyclerView.Adapter 及其方法的详细内容。

重要事项:

如果列表大小巨大,diff 运算操作会耗费极大量时间,因此建议你在后台线程运行该方法来获取 DiffUtil#DiffResult 后在主线程将其应用到 RecyclerView 上。同时由于实现限制,列表的最大大小为 2²⁶

性能

DiffUtil 需要 O(N) 空间来查找两个列表之间的最小添加和删除操作数。它的预期性能是 O(N + D²) 其中 N 是添加和删除项目的总数,D 是编辑脚本的长度。你可以浏览 Android 的官方页面 以获取更多性能数据。

你可以在 GitHub 上找到上述 DiffUtil 示例的参考实现。

原文链接

Last Modified: June 21, 2021