1. 程式人生 > >RecyclerView之AsyncListDiffer

RecyclerView之AsyncListDiffer

參考連結:

https://blog.csdn.net/IO_Field/article/details/79804299

https://developer.android.com/reference/android/support/v7/recyclerview/extensions/ListAdapter

https://developer.android.google.cn/reference/android/support/v7/recyclerview/extensions/AsyncListDiffer

一、ListAdapter

當資料量不大時,我們可以在UI執行緒中直接更新資料,但是當資料量大時這就比較尷尬了,我們需要自己放在子執行緒操作,然後再回UI執行緒更新頁面。在7.0上引入 DiffUtil 之後,現在 Google 又在最新的v7的27.1.1相容包中加入官方的支援,就是 ListAdapter。

ListAdapter 是對 RecyclerView 傳統 Adapter 的一個拓展,在其內建立了 AsyncListDiffer 的示例,以便在後臺執行緒中使用 DiffUtil 計算新舊資料集的差異。即在子執行緒中利用 DiffUtil 比較資料,並在 UI 執行緒更新。

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 差異。

/**
 * 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) 方法

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<Any>) 方法。

——樂於分享,共同進步,歡迎補充
——Any comments greatly appreciated
——誠心歡迎各位交流討論!QQ:1138517609
——CSDN:https://blog.csdn.net/u011489043
——簡書:https://www.jianshu.com/u/4968682d58d1
——GitHub:https://github.com/selfconzrr