RecyclerView之AsyncListDiffer

一、ListAdapter

当数据量不大时,我们可以在UI线程中直接更新数据,但是当数据量大时这就比较尴尬了,我们需要自己放在子线程操作,然后再回UI线程更新页面。在7.0上引入 DiffUtil 之后,现在 Google 又在最新的v7的27.1.1兼容包中加入官方的支持,就是 ListAdapter。

ListAdapter 是对 RecyclerView 传统 Adapter 的一个拓展,在其内创建了 AsyncListDiffer 的示例,以便在后台线程中使用 DiffUtil 计算新旧数据集的差异。即在子线程中利用 DiffUtil 比较数据,并在 UI 线程更新。

java 复制代码
public abstract class ListAdapter<T, VH extends RecyclerView.ViewHolder>
        extends RecyclerView.Adapter<VH> {
    private final AsyncListDiffer<T> mHelper;

    @SuppressWarnings("unused")
    protected ListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
        mHelper = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
                new AsyncDifferConfig.Builder<>(diffCallback).build());
    }

    @SuppressWarnings("unused")
    protected ListAdapter(@NonNull AsyncDifferConfig<T> config) {
        mHelper = new AsyncListDiffer<>(new AdapterListUpdateCallback(this), config);
    }

    /**
     * Submits a new list to be diffed, and displayed.
     * <p>
     * If a list is already being displayed, a diff will be computed on a background thread, which
     * will dispatch Adapter.notifyItem events on the main thread.
     *
     * @param list The new list to be displayed.
     */
    @SuppressWarnings("WeakerAccess")
    public void submitList(List<T> list) {
        mHelper.submitList(list);
    }

    @SuppressWarnings("unused")
    protected T getItem(int position) {
        return mHelper.getCurrentList().get(position);
    }

    @Override
    public int getItemCount() {
        return mHelper.getCurrentList().size();
    }
}

ListAdapter 用法跟普通的 RecyclerView 并无太大差别。

一共2个构造方法,区别在于第一个构造方法可以自己指定执行的线程

submitList(),用于提交新数据,更新UI

数据比较操作的线程在 AsyncListDiffer 中实现。需要注意的一点时,通过 AsyncListDiffer 返回的 List 是一个 UnmodifiableList,意味着不能改变长度。

二、AsyncListDiffer

前面我们已经看到 DiffUtil 如何协助 RecyclerView 进行UI更新。它有一个缺陷就是 DiffUtil 在计算新旧数据集差异时需要开启线程,而在更新UI时又要在主线程。通常做法:

  • Thread + Handler
  • RxJava

虽然这样可以实现,但又显得笨拙。在support-v7:27.1.0又新增了一个 DiffUtil 的封装类 - AsyncListDiffer,它在后台线程使用 DiffUtil 计算旧数据集->新数据集的最小量,同时又在主线程中将更新操作的最小量分发给 ListUpdateCallback,以完成数据更新,从而弥补 DiffUtil 的缺陷。

DiffUtil.ItemCallback(需要升级 RecyclerView 的 support 库)

和使用 DiffUtil 不同的是,我们需要自定义继承自 ItemCallback 的类,而不是 Callback 类。与 DIffUtil.Callback 方法类似,DiffUtil 在计算旧数据集→新数据集时会回调它。在 DiffUtil.Callback 中,提供两种处理角色: Item 的列表索引和 Item 差异,而在 DiffUtil.ItemCallback 中只处理 Item 差异。

java 复制代码
/**
 * DiffUtil 在计算两个列表之间的非空 Item 的差异时使用的回调类。
 * DiffUtil.Callback 提供两种处理角色 - 列表索引和 item 差异。而 DiffUtil.ItemCallback 只处理后者,它允许从UI和内容特定的差异代码中分离索引到数组或者 List 中。
 */
public abstract static class ItemCallback<T> {
    /**
     * 用来判断两个对象是否是相同的 Item
     * 例如,如果你的 Item 有唯一的id字段,这个方法就判断 id 是否相等。
     */
    public abstract boolean areItemsTheSame(T oldItem, T newItem);

    /**
     * 当它想要检查两个 Item 是否具有相同的数据时由 DiffUtil 调用。
     * DiffUtil 使用返回的信息来判断内容是否已更改
     * DiffUtil 用这个方法替代 equals 方法去检查是否相等,以便根据UI更改其行为
     * 例如,如果你用 RecyclerView.Adapter 配合 DiffUtil 使用,需要返回 Item 的视觉表现是否相同。
     * 这个方法仅仅在 areItemsTheSame() 返回true时,才调用。
     * 
     * 返回true,表示新旧数据集中,当前位置的item内容相同,否则,不同
     */
    public abstract boolean areContentsTheSame(T oldItem, T newItem);

    /**
     * 当 areItemsTheSame(int, int) 返回true并且 areContentsTheSame(int, int) 返回false时,DiffUtil 才会调用此方法,以获取该item改变的 payload
     * 例如,如果你用 RecyclerView 配合 DiffUtils,你可以返回这个Item改变的那些字段,RecyclerView.ItemAnimator 可以使用哪些信息执行动画
     * 默认的实现是返回null
     * 返回一个代表着新老item的改变内容的 payload 对象
     */
    @SuppressWarnings({"WeakerAccess", "unused"})
    public Object getChangePayload(T oldItem, T newItem) {
        return null;
    }
}

AsyncDifferConfig

AsyncDifferConfig 是 ListAdapter、AsyncListDiffer 和后台线程的配置对象。在其内,至少定义一个 DiffUtil.ItemCallback,用于 DiffUtil 在计算旧数据集->新数据集时回调。

AsyncDifferConfig.Builder

AsyncDifferConfig.Builder 是 AsyncDifferConfig 构建类,主要是用来配置 DiffUtil 计算数据集差异时的后台线程和 DiffUtil.ItemCallback 回调。

AsyncDifferConfig.Builder (DiffUtil.ItemCallback diffCallback)

在创建 AsyncDifferConfig.Builder 是必须传递一个 DiffUtil.ItemCallback 对象,用于 DiffUtil 计算数据集差异时回调。当 AsyncDifferConfig.Builder 调用 build() 创建 AsyncDifferConfig 对象时,如果未设置指定的后台线程,那么将自动创建一个主线程 Executor 和长度为2的线程池。也就是说 AsyncDifferConfig 持有一个长度为2的线程池,由所有的 AsyncListDiffer 对象共用。如果想指定DiffUtil 计算的后台线程,可调用 setBackgroundThreadExecutor(Executor executor) 方法指定

每次在更新数据集时,都需要调用 AsyncListDiffer 的 submitList(List newList) 方法

java 复制代码
1   public void submitList(final List<T> newList) {
//  2-5行:首先判断 newList 与 AsyncListDiffer 中缓存的数据集 mList 是否为同一个对象,如果是的话,直接返回。也就是说,调用 submitList() 方法所传递数据集时,需要new一个新的List。
2       if (newList == mList) {
3            // nothing to do
4            return;
5        }
6
7        // 定义了变量 runGeneration,用于缓存当前预执行线程的次数的最大值
8        final int runGeneration = ++mMaxScheduledGeneration;
9
//  10-16行:判断 newList 是否为null。若 newList 为 null,将移除所有 Item 的操作并分发给 ListUpdateCallback,mList 置为 null,同时将只读List - mReadOnlyList 清空
10        if (newList == null) {
11            // noinspection ConstantConditions
12            mUpdateCallback.onRemoved(0, mList.size());
13            mList = null;
14            mReadOnlyList = Collections.emptyList();
15            return;
16        }
17
//  18-24行:判断 mList 是否为null。若 mList 为null,表示这是第一次向 Adapter 添加数据集,此时将添加最新数据集操的作分发给 ListUpdateCallback,将 mList 设置为 newList,同时以 newList 为原本创建只读List副本 - mReadOnlyList
18        if (mList == null) {
19            // fast simple first insert
20            mUpdateCallback.onInserted(0, newList.size());
21            mList = newList;
22            mReadOnlyList = Collections.unmodifiableList(newList);
23            return;
24        }
25
26        final List<T> oldList = mList;


//  27-52行:syncDifferConfig 对象获取后台线程,并运行该后台线程。在该线程中,创建一个新的 DiffUtil.Callback() 回调,同时 DiffUtil 在计算旧数据集->新数据集的最小量时回调该 Callbcak。而这个 DiffUtil.Callback 在处理 Item 差异时,调用的是所传递过去的 DiffUtil.ItemCallback 实例中的 areItemsTheSame() 方法和areContentsTheSame()。这里需要注意的一点是:在这个 DiffUtil.Callback 中,并没有重写 getChangePayload() 方法,这不仅意味着它并没有实现高效地局部绑定,同时自定义 DiffUtil.ItemCallback 时也没有必要实现 getChangePayload() 方法。
27        mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
28            @Override
29            public void run() {
30                final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
31                   @Override
32                    public int getOldListSize() {
33                        return oldList.size();
34                    }
35
36                    @Override
37                    public int getNewListSize() {
38                        return newList.size();
39                    }
40
41                    @Override
42                    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
43                        return mConfig.getDiffCallback().areItemsTheSame(
44                                oldList.get(oldItemPosition), newList.get(newItemPosition));
45                    }
46
47                    @Override
48                    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
49                        return mConfig.getDiffCallback().areContentsTheSame(
50                                oldList.get(oldItemPosition), newList.get(newItemPosition));
51                    }
52                });
53
//  54-63行:当 DiffUtil 计算新旧数据集差异之后,首先判断 runGeneration 与当前预执行线程的次数的最大值是否相等,如果相等,将 mList 设置为 newList,同时以 newList 为原本重建创建只读List副本 - mReadOnlyList,在主线程中将更新操作分发给 ListUpdateCallback。如果不相等,表示在此线程运行以后,再一次调用了submitList()方法用于传递新的数据集,此时将不做任何处理,待DiffUtil计算结束以后,再做相应处理。
54                mConfig.getMainThreadExecutor().execute(new Runnable() {
55                    @Override
56                    public void run() {
57                        if (mMaxScheduledGeneration == runGeneration) {
58                            latchList(newList, result);
59                        }
60                    }
61                });
62            }
63        });
64    }

65   private void latchList(@NonNull List<T> newList, @NonNull DiffUtil.DiffResult diffResult) {
66        mList = newList;
67        mReadOnlyList = Collections.unmodifiableList(newList);
68        diffResult.dispatchUpdatesTo(mUpdateCallback);
69    }

值得注意的两点就是:

  1. 调用 submitList() 方法传递数据集时,需要new一个新的List。
  2. 在这个 DiffUtil.Callback 中,并没有重写 getChangePayload() 方法,这不仅意味着它并没有实现高效地局部绑定,同时自定义 DiffUtil.ItemCallback 时也没有必要实现 getChangePayload() 方法。

了解了 submitList() 方法的实现方式,如果对没有实现高效地局部绑定的问题,完全可以自定义 AsyncListDiffer,然后在创建 DiffUtil.Callback() 回调时,重写 getChangePayload() 方法即可。当然在 Apdaer 中也需要重写 onBindViewHolder(holder: DiffViewHolder, position: Int, payloads: MutableList) 方法。

参考链接:

blog.csdn.net/IO_Field/ar...

developer.android.com/reference/a...

developer.android.google.cn/reference/a...

------乐于分享,共同进步,欢迎留言讨论
------Treat Warnings As Errors
------Any comments greatly appreciated
------Talking is cheap, show me the code
------CSDN:blog.csdn.net/u011489043
------GitHub:github.com/selfconzrr

相关推荐
Q_19284999064 分钟前
基于Spring Boot的电影售票系统
java·spring boot·后端
我要学编程(ಥ_ಥ)1 小时前
初始JavaEE篇 —— 网络原理---传输层协议:深入理解UDP/TCP
java·网络·tcp/ip·udp·java-ee
就爱学编程1 小时前
重生之我在异世界学编程之C语言:数据在内存中的存储篇(下)
java·服务器·c语言
yuanbenshidiaos1 小时前
C++--------------树
java·数据库·c++
俎树振2 小时前
Java数组深入解析:定义、操作、常见问题与高频练习
java·开发语言
花心蝴蝶.2 小时前
Map接口 及其 实现类(HashMap, TreeMap)
java·数据结构
小天努力学java2 小时前
【面试系列】深入浅出 Spring
java·spring·面试
Just_Paranoid2 小时前
解析 Java 项目生成常量、变量和函数 Excel 文档
java·python·正则表达式·excel·javadoc
阿垂爱挖掘2 小时前
34 - Java 8 Stream
java
debug_cat2 小时前
AndroidStudio Ladybug中编译完成apk之后定制名字kts复制到指定目录
android·android studio