现在可以和 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
类,它使用 EmployeeRecyclerViewAdapter
和 EmployeeDiffCallback
来对 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()
返回的结果通过 DiffResult
的 notifyItemRangeChanged(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
示例的参考实现。