DiffUtil 是 Android 中用于核算两个列表之间差异的实用工具类。它能够优化 RecyclerView 的改写操作,仅改写需求更新的部分,然后进步功能并削减不必要的操作。

本篇博客将从简略到高档,介绍运用 DiffUtil 的根本流程以及一些高档用法,协助开发者更好地运用 DiffUtil。

什么是 DiffUtil?

DiffUtil 是一个用于核算两个列表之间差异的实用工具类。它经过比较两个列表的元素,找出它们之间的差异,并生成更新操作的列表,以便进行最小化的更新操作

为什么需求 DiffUtil?

这两天在做IM模块,写IM会话列表需求时发现了一个必要的优化点。

谈天列表页面用于显示一切的谈天记录。在这个页面中,每一条谈天记录都包括对方的头像、昵称、消息内容和时刻戳等信息。为了进步用户体会,咱们期望谈天列表能够完成以下功用:

  1. 支撑实时改写:当有新的谈天记录抵达时,列表能够当即进行更新,而不需求手动改写页面;
  2. 支撑快速滑动:用户能够快速滑动列表,查看更多的谈天记录;
  3. 支撑查找:用户能够经过查找功用查找特定的谈天记录。

完成以上功用,需求谈天列表能够快速响应数据集的改动,而且能够高效地更新界面。而在大多数状况下,谈天列表的数据集很可能是动态改动的,因而咱们需求一种高效的算法来比较旧数据集和新数据集之间的差异,而且只更新产生了改动的列表项。这时,DiffUtil 就变得十分重要了。

在谈天列表的需求场景中,假如咱们不运用 DiffUtil,每次数据集产生改动时,都需求调用 RecyclerView.Adapter 中的 notifyDataSetChanged() 办法来改写整个列表。这样做尽管简略,可是会导致列表的滑动方位和状态全部被重置,用户体会十分不友好。而假如运用 DiffUtil,咱们能够仅仅更新数据会集产生改动的那些列表项,这样能够极大地进步 RecyclerView 的功能,而且保持列表的滑动方位和状态不变,然后提高用户体会

当然这仅仅一个需求场景,咱们能够理解为:

在运用 RecyclerView 时,假如数据集产生改动,咱们通常需求调用 notifyDataSetChanged() 或许 notifyItemRangeChanged() 等办法进行改写操作。可是这样做会导致整个列表都被改写,即便只有一部分数据产生了改动,也会重新制作整个列表,形成功能浪费。DiffUtil 能够协助咱们只更新产生改动的部分,然后削减不必要的改写操作,进步功能。当咱们面对这个问题时能够考虑DiffUtil

DiffUtil 的用法

DiffUtil 的运用过程如下:

  1. 创立两个列表,别离表明旧数据集和新数据集。
  2. 创立一个 DiffUtil.Callback 目标,完成其间的办法,用于比较旧数据集和新数据集的元素,并核算它们之间的差异。
  3. 调用 DiffUtil.calculateDiff() 办法,传入上一步创立的 DiffUtil.Callback 目标,核算差异,并回来更新操作的列表。
  4. 运用回来的更新操作列表,更新 RecyclerView 的数据集。

创立 DiffUtil.Callback

DiffUtil.Callback 是 DiffUtil 的核心类,用于比较旧数据集和新数据集的元素,并核算它们之间的差异。它包括四个笼统办法,需求咱们自行完成。

  1. public abstract int getOldListSize():获取旧数据集的巨细。
  2. public abstract int getNewListSize():获取新数据集的巨细。
  3. public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition):判别旧数据会集的某个元素和新数据会集的某个元素是否代表同一个实体。
  4. public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition):判别旧数据会集的某个元素和新数据会集的某个元素的内容是否相同。

除此之外,DiffUtil.Callback 还有两个可选的办法,能够用于进一步优化核算过程:

  1. public Object getChangePayload(int oldItemPosition, int newItemPosition)`:获取旧数据会集的某个元素和新数据会集的某个元素之间的差异信息。假如这两个元素相同,可是内容产生改动,能够经过这个办法获取它们之间的差异信息,然后只更新需求改动的部分,削减不必要的更新操作。
  2. public boolean getMoveDetectionFlag():设置是否开启移动操作的检测。假如设置为 true,DiffUtil 会检测数据会集元素的移动操作,并生成移动操作的更新列表。可是,开启移动操作的检测会添加核算量,可能会影响功能。

运用 DiffUtil

DiffUtil 的另一个好处就是和Adapter高度解耦,在原油的Adapter不动的状况下也能完成需求,下面是一个完好的比方,留意不一定要动Adapter的代码啊

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
    private List<String> mOldList;
    private List<String> mNewList;
    // 结构办法,初始化数据集
    public MyAdapter(List<String> oldList, List<String> newList) {
        mOldList = oldList;
        mNewList = newList;
    }
    // ViewHolder,用于缓存列表项的视图
    public static class ViewHolder extends RecyclerView.ViewHolder {
        public TextView mTextView;
        public ViewHolder(View itemView) {
            super(itemView);
            mTextView = itemView.findViewById(R.id.text_view);
        }
    }
    // 创立 ViewHolder
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout, parent, false);
        return new ViewHolder(view);
    }
    // 绑定 ViewHolder
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        String text = mNewList.get(position);
        holder.mTextView.setText(text);
    }
    // 获取数据集巨细
    @Override
    public int getItemCount() {
        return mNewList.size();
    }
    // 更新数据集
    public void updateList(List<String> newList) {
        // 核算差异
        DiffUtil.Callback callback = new MyCallback(mOldList, newList);
        DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback);
        // 更新数据集
        mOldList = mNewList;
        mNewList = newList;
        result.dispatchUpdatesTo(this);
    }
    // 自定义的 DiffUtil.Callback
    private static class MyCallback extends DiffUtil.Callback {
        private List<String> mOldList;
        private List<String> mNewList;
        public MyCallback(List<String> oldList, List<String> newList) {
            mOldList = oldList;
            mNewList = newList;
        }
        @Override
        public int getOldListSize() {
            return mOldList.size();
        }
        @Override
        public int getNewListSize() {
            return mNewList.size();
        }
        @Override
        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
            return mOldList.get(oldItemPosition).equals(mNewList.get(newItemPosition));
        }
        @Override
        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
            return mOldList.get(oldItemPosition).equals(mNewList.get(newItemPosition));
        }
    }
}

在上述示例中,咱们创立了一个自定义的 Adapter,并在其间完成了 updateList() 办法,用于更新数据集。在 updateList() 办法中,咱们先运用自定义的 DiffUtil.Callback 对旧数据集和新数据集进行差异核算,然后将新数据集赋值给成员变量 mNewList,并将核算得到的差异信息经过 result.dispatchUpdatesTo(this) 办法更新到 RecyclerView 中。

在 MyCallback 中,咱们需求完成 DiffUtil.Callback 中的四个办法,其间 areItemsTheSame() 和 areContentsTheSame() 办法别离用于判别两个列表项是否代表同一个目标,以及这两个目标的内容是否相同。假如这两个办法都回来 true,那么 DiffUtil 以为这两个列表项相同,不需求进行更新操作;假如其间恣意一个办法回来 false,那么 DiffUtil 以为这两个列表项不同,需求进行更新操作。

在这个比方中,咱们运用了字符串列表作为数据集,因而能够直接运用 equals() 办法比较两个字符串是否相等。假如咱们运用的是自定义目标,那么需求根据详细状况完成 areItemsTheSame() 和 areContentsTheSame() 办法。

除了以上示例中的办法,DiffUtil 还供给了一些其他的办法和类,用于愈加高档的差异核算。例如:

  • public static DiffUtil.DiffResult calculateDiff(DiffUtil.Callback callback, boolean detectMoves):同 calculateDiff() 办法,但能够指定是否开启移动操作的检测。
  • public static List diff(List oldList, List newList, ItemCallback callback):一个愈加灵活的差异核算办法,能够自定义目标的比较方式,而且支撑异步核算。
  • public static class AsyncDiffResult extends AsyncTask<Void, Void, DiffUtil.DiffResult>:异步核算差异的工具类,能够在子线程中进行差异核算,并在主线程中更新 UI。

支撑异步核算

在处理大量数据时,DiffUtil 的差异核算可能会比较耗时,然后影响运用的响应速度。为了防止这种状况,咱们能够将差异核算放在一个异步使命中进行。DiffUtil 供给了 AsyncDiffResult 类来支撑异步核算,详细运用办法如下

class ChatListAdapter : RecyclerView.Adapter<ChatListAdapter.ViewHolder>() {
    private var messageList: List<ChatMessage> = emptyList()
    fun submitListAsync(newList: List<ChatMessage>) {
        val callback = ChatMessageDiffCallback(messageList, newList)
        val asyncTask = object : AsyncTask<Void, Void, DiffUtil.DiffResult>() {
            override fun doInBackground(vararg voids: Void): DiffUtil.DiffResult {
                return DiffUtil.calculateDiff(callback)
            }
            override fun onPostExecute(diffResult: DiffUtil.DiffResult) {
                messageList = newList
                diffResult.dispatchUpdatesTo(this@ChatListAdapter)
            }
        }
        asyncTask.execute()
    }
    // ...
}

运用 AsyncTask 来异步核算差异,并在核算完成后更新数据集。假如咱们的数据集比较大,能够运用这种办法来防止堵塞主线程。留意,在运用异步核算时,咱们不能直接调用 notifyDataSetChanged() 办法来改写列表,而是需求经过 DiffUtil.DiffResult 的 dispatchUpdatesTo() 办法来更新列表。

DiffUtil 虽好可不要贪杯哦

  1. 尽量运用不行变数据目标。DiffUtil 是经过比较两个数据目标的引证来判别它们是否相同的,因而假如咱们在列表中运用可变数据目标,那么很简单呈现引证相同但内容不同的状况,然后导致列表的更新呈现问题。所以,在运用 DiffUtil 时,最好运用不行变数据目标,或许在可变数据目标中保证引证的唯一性。
  2. 留意数据目标的 equals() 办法的完成。假如咱们运用的是自定义的数据目标,那么需求确保它的 equals() 办法的完成是正确的,否则会导致 DiffUtil 核算的不准确。详细来说,equals() 办法应该正确地比较数据目标的一切字段。
  3. 尽量防止在列表中运用动态数据。DiffUtil 的差异核算是基于静态数据的,假如咱们在列表中运用了动态数据,比方时刻戳、随机数等,那么可能会导致每次核算的成果不同,然后影响列表的更新效果。所以,假如需求在列表中运用动态数据,能够将其核算成果缓存下来,以防止影响列表的更新。
  4. 尽量防止对数据进行频频的更新。尽管 DiffUtil 能够十分高效地核算出数据集的差异,可是假如咱们对数据进行频频的更新,那么就会导致 DiffUtil 不断地进行核算,然后影响运用的功能。所以,在运用 DiffUtil 时,尽量防止对数据进行频频的更新,能够将数据集的更新批量处理,或许运用合适的更新策略,如增量更新、局部更新等。
  5. 留意数据集的次序。DiffUtil 的差异核算是基于数据集的次序的,假如数据集的次序产生了改动,那么就需求重新核算差异。所以,在运用 DiffUtil 时,需求留意数据集的次序,尽量防止频频地对数据集进行排序等操作。
  6. 防止在 UI 线程中进行差异核算。DiffUtil 的差异核算可能会比较耗时,假如在 UI 线程中进行核算,就会导致运用的卡顿,影响用户体会。所以,在运用 DiffUtil 时,最好将差异核算放在一个异步使命中进行,或许运用其他方式来防止堵塞 UI 线程。

总结

DiffUtil 是一个用于核算两个数据集之间差异的工具类,能够协助咱们削减不必要的更新操作,进步 RecyclerView 的功能。运用 DiffUtil 需求完成 DiffUtil.Callback 接口,并根据详细状况完成其间的办法。除了根本的差异核算办法,DiffUtil 还供给了许多其他的办法和类,能够根据实际需求挑选运用。