RecyclerView中DiffUtil的一些注意事項

節能刷新

移動設備屏幕大小有限(不得不說我是頑固的小屏愛好者,大於5.5寸難以接受,時代已經拋棄我了哈哈),列表(List)能夠說是一個出現很是高頻的交互設計。大多數狀況下咱們的列表不只僅是一次性加載本地數據,而要應付來自網絡的各類動態內容,多是增長、刪除等操做。java

在Android開發中,一個耳熟能詳的方法就是 notifyDataSetChanged ,在適配器(Adapter)的設計模式下,每當咱們的列表數據發生變動時,就須要調用此方法來更新UI。然而,這個方法並不「節能」,它會同時刷新列表中的全部item,包括那些並無變化的數據,這樣就帶來不少計算資源的浪費。要知道,從你的一個 setText 或者 setImageResource 方法調用到最終呈現到屏幕上,軟件到硬件,中間經歷了很是複雜的過程。基於能省則省的移動開發原則,有沒有更好的辦法呢?算法

DiffUtil用起來

谷歌確實也考慮到了這個問題,因此不知道在何時(暫時沒有去查閱)推出了DiffUtil這個解決方案。在RecyclerView的依賴包下面,能夠看到,除了DiffUtil,還有異步處理數據等一系列有趣的工具。 設計模式

在這裏插入圖片描述
DiffUtil的運用邏輯很是簡單,大體以下:

  • 實現對比新舊數據的方法(相似比較器),這樣DiffUtil便知道當新數據來臨時,該不應更新某個item。
  • 更新數據時,把新舊數據丟給DiffUtil,底層會根據你實現的對比方法,利用一種差分算法自動計算出差別,最後局部更新到UI。

這樣作的好處就是避免了沒必要要的UI更新,DiffUtil計算出差別以後,只刷新產生變更的item。具體地,咱們能夠在Adapter的 onBindViewHolder 方法打斷點或者日誌觀察,或者調用 registerAdapterDataObserver 方法監聽item的各類操做狀況。其次,之前的 notifyDataSetChanged 方法因爲會刷新整個列表因此沒有原生的動畫效果,而DiffUtil內部最終調用了各類 notifyItemXXX 方法。數組

DiffUtil的使用也很簡單:網絡

一、先實現比較新舊數據的回調,能夠是一個獨立的類,也能夠寫成Adapter的內部類:異步

public class BaseXXXAdapter<T> extends RecyclerView.Adapter {
    // ...

    private class DiffCallback extends DiffUtil.Callback {
        private List<T> oldData, newData;

        DiffCallback(List<T> oldData, List<T> newData) {
            this.oldData = oldData;
            this.newData = newData;
        }

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

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

        @Override
        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
            T oldT = oldData.get(oldItemPosition);
            T newT = newData.get(newItemPosition);
            // 實際狀況最好是在此處對比新舊數據的id(好比用戶uid),這裏爲了方便示例直接equals對象了
            // 若此處返回true,則DiffUtil會再調用下面的areContentsTheSame方法,進一步對比UI是否有變化
            // 若此處返回false,則說明id都不一樣,確定不是一個item
            return Objects.equals(oldT, newT);
        }

        @Override
        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
            // TODO 比較新舊數據(主要是UI展現內容)是否相同,這裏爲了方便示例直接返回true
            return true;
        }
    }
}
複製代碼

二、而後在Adapter內部實現一個update數據的方法:ide

@Override
    public void updateData(List<T> newData) {
        DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffCallback(getData(), newData));
        // 這裏的getData即表示獲取整個列表的數據,自行實現便可
        getData().clear();
        getData().addAll(newData);
        result.dispatchUpdatesTo(this);
    }
複製代碼

注意這裏的 dispatchUpdatesTo 能夠在clear以前,也能夠在addAll以後,實際效果暫未發現什麼區別,以前查閱資料包括官方示例也都是最後執行dispatch,姑且認爲這樣算標準吧。工具

三、……咦,怎麼才兩步,確實就這麼簡單。重點仍是 areItemsTheSameareContentsTheSame 方法,後者大部分時候只須要對比每一個item上UI展現出來的數據便可,由於用戶只關心眼見的內容。動畫

解決使用後產生的問題

咱們會發如今上面的使用示例中,updateData 方法內部對原數據進行了清除和添加的操做,這會致使一個問題即是:列表數據集合中的對象已經變了,即便其某項對應的UI內容沒有發生變化ui

舉個例子,一個通信錄列表裏面有 [小明, 小紅] 兩我的,對應內存地址爲 [a1, a2],如今經過上述 updateData 方法更新了通信錄列表,UI內容變成了 [小王, 小紅],對應內存地址爲 [b1, b2]。對用戶來講小紅這個item看上去沒有發生變化,但其實對應的數據類對象已經不一樣。並且此時 onBindViewHolder 方法只會觸發一次,將小明更新成小王,而不會觸發小紅那個position對應的 onBindViewHolder

上述細節很關鍵,若是開發過程當中綁定(bind)數據不恰當的話,就容易形成各類奇異問題,好比網上資料最多的DiffUtil致使item點擊事件數據錯位問題、數組越界崩潰問題等等。

這裏的「不恰當」,絕大部分狀況下,總結出來:其實指的就是在 onBindViewHolder 方法中持有了某個位置(position)對應數據的不可變對象。最多見的誤用示例就是在 onBindViewHolder 中設置某些控件的點擊事件並引用數據對象:

// 此處假設item的數據類爲User
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        MyItemViewHolder h = (MyItemViewHolder) holder;
        User user = getData().get(position);
        h.mNameTextView.setOnClickListener(v -> {
            // 第2種寫法:User user = getData().get(position);
            // 假設這裏是點擊item跳轉到該User對應的我的主頁界面
            startWebView(user.getHomePageUrl());
        });
    }
複製代碼

在不接入DiffUtil以前,上面這段代碼沒有任何問題,由於咱們都是使用 notifyDataSetChanged 方法來更新UI,每次更新調用到 onBindViewHolder 時,點擊事件都會從新設置,get出來的user對象天然也是最新的。一旦咱們使用了DiffUtil,就會出問題了。

回到上面小王綠了小明的例子,在咱們的 updateData 方法執行後,若是咱們只對比了user的名字這個屬性(其實也只須要對比這個屬性),那麼小紅那一個item就不會觸發對應的 onBindViewHolder ,即小紅的點擊事件回調裏,仍然持有着舊數據集的user對象(對應那個內存地址a2)。但實際上小紅應該對應 b2 那個內存了,這就形成 a2 內存沒法釋放,問題是否是顯得有點嚴重了。

有同窗說無所謂呀,反正點擊事件依然有效。那若是我說網絡數據刷新下來小紅的 homePageUrl 變了呢?是否是還得把這個屬性加入DiffUtil的對比方法中?這樣最終會致使小紅的 onBindViewHolder 方法也執行,跟 notifyDataSetChanged 豈不是沒什麼兩樣了?

此外,若get對象寫成註釋中的第2種寫法,且列表第0個位置的item被刪了呢?小紅頂上去變成了第0個,此時因爲小紅的UI內容沒變,只是位置變了,因此 onBindViewHolder 依然不會執行。以上面的示例代碼來看,當再次點擊小紅時,就會直接出現數組越界的異常。由於position仍是以前的1,而此時小紅的position已經爲0。

顯然上述出現的這些問題不符合谷歌的設計初衷,也不符合咱們使用DiffUtil的初衷。其實解決辦法很簡單,就是要對 onBindViewHolder 方法有一個正確的認知,其原則就是:

  • onBindViewHolder 只作UI內容的更新,如 setTextsetImageXXX 等方法。作到數據對象一次性使用。
  • 不要跨做用域持有與位置(position)相關的數據,好比每一個item的數據對象。尤爲就是避免在 onBindViewHolder 中設置點擊事件監聽。

正確的點擊事件監聽仍是參照以下形式比較好:

// 好比這是某個Base適配器類
public class BaseXXXAdapter<T> extends RecyclerView.Adapter {
    // ...
    private View.OnClickListener mOnClickListener;
    private View.OnLongClickListener mOnLongClickListener;
    private OnItemClickListener mOnItemClickListener;

    public interface OnItemClickListener {
        void onItemClick(View view, RecyclerView.ViewHolder holder, int position);

        void onItemLongClick(View view, RecyclerView.ViewHolder holder, int position);
    }

    public BaseXXXAdapter(Context context) {
        // ...
        mOnClickListener = v -> {
            RecyclerView.ViewHolder h = (RecyclerView.ViewHolder) v.getTag();
            int pos = h.getAdapterPosition();
            if (mOnItemClickListener != null) {
                mOnItemClickListener.onItemClick(v, h, pos);
            }
        };
        mOnLongClickListener = v -> {
            RecyclerView.ViewHolder h = (RecyclerView.ViewHolder) v.getTag();
            int pos = h.getAdapterPosition();
            if (mOnItemClickListener != null) {
                mOnItemClickListener.onItemLongClick(v, h, pos);
            }
            return true;
        };
    }

    public void setOnItemClickListener(OnItemClickListener clickListener) {
        this.mOnItemClickListener = clickListener;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // ...省略holder實例化
        holder.itemView.setTag(holder); // 把holder當tag存
        holder.itemView.setOnClickListener(mOnClickListener);
        holder.itemView.setOnLongClickListener(mOnLongClickListener);
        return holder;
    }
}

// 繼承實現的實際業務Adapter
public class XXXAdapter extends BaseXXXAdapter<User> {
    public XXXAdapter(Context context) {
        setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(View view, RecyclerView.ViewHolder holder, int position) {
                MyItemViewHolder h = (MyItemViewHolder) holder;
                // 每次點擊都保證了爲對應位置的數據,不再用擔憂數據錯位問題了
                User user = getData().get(position);
            }

            @Override
            public void onItemLongClick(View view, RecyclerView.ViewHolder holder, int position) {
                // ...
            }
        });
    }
}
複製代碼
相關文章
相關標籤/搜索