Android AsyncListDiffer-RecyclerView最好的伙伴

版權(quán)聲明:本文已授權(quán)微信公眾號:Android必修課撵术,轉(zhuǎn)載請申明出處

自Android5.0以來钠乏,RecyclerView漸漸取代ListView成為Android開發(fā)中使用最多的列表控件代箭,對于RecyclerView的使用相信大家都不陌生墩划,但對于RecyclerView的高效刷新,卻是很多人不知道的嗡综。

簡單粗暴的刷新方式

Adapter.notifyDataSetChanged();

這種方式想必是大家曾經(jīng)用的最多的一種刷新Adapter的方式乙帮,它的缺點很明顯:

  • 無腦刷新整個RecyclerView可視區(qū)域,每個item重繪极景,如果你的onBindViewHolder邏輯處理稍微復(fù)雜一些察净,則容易造成卡頓
  • 無法觸發(fā)RecyclerView的item動畫,用戶體驗極差盼樟。

局部刷新方式

為了解決上述問題氢卡,RecyclerView推出了局部刷新的方式

Adapter.notifyItemChanged(int)
Adapter.notifyItemInserted(int)
Adapter.notifyItemRangeChanged(int, int)
Adapter.notifyItemRangeInserted(int, int)
Adapter.notifyItemRangeRemoved(int, int)

局部刷新只會刷新指定position的item,這樣完美解決了上述簡單粗暴刷新方式的缺點晨缴,但是:

  • 局部刷新需要指定item的position译秦,如果你只更新了一條數(shù)據(jù),那么你可以很容易知道position位置击碗,但是如果你更新的是整個列表筑悴,你需要計算出所有你需要刷新的position,那么這將是一場災(zāi)難

DiffUtil

Google似乎也注意到了這一點稍途,因此在support-recyclerview-v7:24.2.0中阁吝,推出了一個用于計算哪些位置需要刷新的工具類:DiffUtil。

使用DiffUtil晰房,有3個步驟

1.自實現(xiàn)DiffUtil.callback

private DiffUtil.Callback diffCallback = new DiffUtil.Callback() {
    @Override
    public int getOldListSize() {
        // 返回舊數(shù)據(jù)的長度
        return oldList == null ? 0 : oldList.size();
    }

    @Override
    public int getNewListSize() {
        // 返回新數(shù)據(jù)的長度
        return newList == null ? 0 : newList.size();
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        // 返回兩個item是否相同
        // 例如:此處兩個item的數(shù)據(jù)實體是User類求摇,所以以id作為兩個item是否相同的依據(jù)
        // 即此處返回兩個user的id是否相同
        return TextUtils.equals(oldList.get(oldItemPosition).getId(), newList.get(oldItemPosition).getId());
    }

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        // 當(dāng)areItemsTheSame返回true時,我們還需要判斷兩個item的內(nèi)容是否相同
        // 此處以User的age作為兩個item內(nèi)容是否相同的依據(jù)
        // 即返回兩個user的age是否相同
        return oldList.get(oldItemPosition).getAge() == newList.get(newItemPosition).getAge();
    }
};

2.計算得到DiffResult

 DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);

3.將DiffResult設(shè)置給Adapter

 // 注意此處一定要將新數(shù)據(jù)設(shè)置給Adapter
 // 否則會造成ui刷新了但數(shù)據(jù)未更新的bug
 mAdapter.setData(newList);
 diffResult.dispatchUpdatesTo(mAdapter);

這樣我們就實現(xiàn)了局部刷新位置的計算和局部刷新的實現(xiàn)殊者,相比notifyDataSetChanged()与境,性能大大提高。

本文到此結(jié)束猖吴?

不不不摔刁,還早著呢,咱們理智分析一下:

  • 首先DiffUtil.calculateDiff()這個方法是執(zhí)行在主線程的海蔽,如果新舊數(shù)據(jù)List比較大共屈,那么這個方法鐵定是會阻塞主線程的
  • 計算出DiffResult后,咱們必須要將新數(shù)據(jù)設(shè)置給Adapter党窜,然后才能調(diào)用DiffResult.dispatchUpdatesTo(Adapter)刷新ui拗引,然而很多人都會忘記這一步。

AsyncListDiff

DiffUtil已經(jīng)很好用了幌衣,但是有上述兩個問題矾削,想必Google的工程師也是看不下去的壤玫,雖然上述兩個問題不難解決,但是很容易遺漏哼凯。

因此Google又推出了一個新的類AsyncListDiff

先來看一波AsyncListDiff的使用方式:

public class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserViewHodler> {
    private AsyncListDiffer<User> mDiffer;

    private DiffUtil.ItemCallback<User> diffCallback = new DiffUtil.ItemCallback<User>() {
        @Override
        public boolean areItemsTheSame(User oldItem, User newItem) {
            return TextUtils.equals(oldItem.getId(), newItem.getId());
        }

        @Override
        public boolean areContentsTheSame(User oldItem, User newItem) {
            return oldItem.getAge() == newItem.getAge();
        }
    };

    public UserAdapter() {
        mDiffer = new AsyncListDiffer<>(this, diffCallback);
    }

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

    public void submitList(List<User> data) {
        mDiffer.submitList(data);
    }

    public User getItem(int position) {
        return mDiffer.getCurrentList().get(position);
    }

    @NonNull
    @Override
    public UserAdapter.UserViewHodler onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_user_list, parent, false);
        return new UserViewHodler(itemView);
    }

    @Override
    public void onBindViewHolder(@NonNull UserAdapter.UserViewHodler holder, int position) {
        holder.setData(getItem(position));
    }

    class UserViewHodler extends RecyclerView.ViewHolder {
        private TextView tvName;
        private TextView tvAge;

        public UserViewHodler(View itemView) {
            super(itemView);
            tvName = itemView.findViewById(R.id.tv_name);
            tvAge = itemView.findViewById(R.id.tv_age);
        }

        public void setData(User data) {
            tvName.setText(data.getName());
            tvAge.setText(String.valueOf(data.getAge()));
        }
    }
}

這里使用了一個簡單的Adapter例子欲间,不做封裝,是為了更好地說明AsyncListDiffer断部。

不難看出猎贴,AsyncListDiffer的使用步驟:

  • 自實現(xiàn)DiffUtil.ItemCallback,給出item差異性計算條件
  • 將所有對數(shù)據(jù)的操作代理給AsyncListDiffer蝴光,可以看到這個Adapter是沒有List數(shù)據(jù)的
  • 使用submitList()更新數(shù)據(jù)她渴,并刷新ui

ok,咱們看一下效果:
首先我們給Adapter設(shè)置數(shù)據(jù)

List<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    users.add(new User(String.valueOf(i), "用戶" + i, i + 20));
}
mAdapter.submitList(users);

然后修改數(shù)據(jù)

List<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    users.add(new User(String.valueOf(i), "用戶" + i, i % 3 == 0 ? i + 10: i + 20));
}
mAdapter.submitList(users);

跑起來看一哈



ok蔑祟,我們看到只有被3整除的position被刷新了惹骂,完美的局部刷新。

那么問題來了做瞪,AsyncListDiffer是如何解決我們上述的兩個問題的呢?

解惑

我們走進(jìn)AsyncListDiffer的源碼看一下:

public class AsyncListDiffer<T> {
    private final ListUpdateCallback mUpdateCallback;
    private final AsyncDifferConfig<T> mConfig;

    public AsyncListDiffer(@NonNull RecyclerView.Adapter adapter,
                @NonNull DiffUtil.ItemCallback<T> diffCallback) {
        mUpdateCallback = new AdapterListUpdateCallback(adapter);
        mConfig = new AsyncDifferConfig.Builder<>(diffCallback).build();
    }

    private List<T> mList;
    private List<T> mReadOnlyList = Collections.emptyList();
    private int mMaxScheduledGeneration;

    public List<T> getCurrentList() {
        return mReadOnlyList;
    }

    public void submitList(final List<T> newList) {
        if (newList == mList) {
            // 如果新舊數(shù)據(jù)相同,則啥事不做
            return;
        }

        // 用于控制計算線程右冻,防止在上一次submitList未完成時装蓬,
        // 又多次調(diào)用submitList,這里只返回最后一個計算的DiffResult
        final int runGeneration = ++mMaxScheduledGeneration;

        if (newList == null) {
            // 如果新數(shù)據(jù)集為空纱扭,此種情況不需要計算diff
            // 直接清空數(shù)據(jù)即可
            // 通知item remove
            mUpdateCallback.onRemoved(0, mList.size());
            mList = null;
            mReadOnlyList = Collections.emptyList();
            return;
        }

        if (mList == null) {
            // 如果舊數(shù)據(jù)集為空牍帚,此種情況不需要計算diff
            // 直接將新數(shù)據(jù)添加到舊數(shù)據(jù)集即可
            // 通知item insert
            mUpdateCallback.onInserted(0, newList.size());
            mList = newList;
            mReadOnlyList = Collections.unmodifiableList(newList);
            return;
        }

        final List<T> oldList = mList;
        // 在子線程中計算DiffResult
        mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
            @Override
            public void run() {
                final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
                    @Override
                    public int getOldListSize() {
                        return oldList.size();
                    }

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

                    @Override
                    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
                        return mConfig.getDiffCallback().areItemsTheSame(
                                oldList.get(oldItemPosition), newList.get(newItemPosition));
                    }

                    @Override
                    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
                        return mConfig.getDiffCallback().areContentsTheSame(
                                oldList.get(oldItemPosition), newList.get(newItemPosition));
                    }
                });
                // 在主線程中更新數(shù)據(jù)
                mConfig.getMainThreadExecutor().execute(new Runnable() {
                    @Override
                    public void run() {
                        if (mMaxScheduledGeneration == runGeneration) {
                            latchList(newList, result);
                        }
                    }
                });
            }
        });
    }

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

線程部分源碼:

private static class MainThreadExecutor implements Executor {
    final Handler mHandler = new Handler(Looper.getMainLooper());
    @Override
    public void execute(@NonNull Runnable command) {
        mHandler.post(command);
    }
}

@NonNull
public AsyncDifferConfig<T> build() {
    if (mMainThreadExecutor == null) {
        mMainThreadExecutor = sMainThreadExecutor;
    }
    if (mBackgroundThreadExecutor == null) {
        synchronized (sExecutorLock) {
            if (sDiffExecutor == null) {
                sDiffExecutor = Executors.newFixedThreadPool(2);
            }
        }
        mBackgroundThreadExecutor = sDiffExecutor;
    }
    return new AsyncDifferConfig<>(
            mMainThreadExecutor,
            mBackgroundThreadExecutor,
            mDiffCallback);
}

ui刷新部分源碼:

public final class AdapterListUpdateCallback implements ListUpdateCallback {
    @NonNull
    private final RecyclerView.Adapter mAdapter;

    public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
        mAdapter = adapter;
    }

    @Override
    public void onInserted(int position, int count) {
        mAdapter.notifyItemRangeInserted(position, count);
    }

    @Override
    public void onRemoved(int position, int count) {
        mAdapter.notifyItemRangeRemoved(position, count);
    }

    @Override
    public void onMoved(int fromPosition, int toPosition) {
        mAdapter.notifyItemMoved(fromPosition, toPosition);
    }

    @Override
    public void onChanged(int position, int count, Object payload) {
        mAdapter.notifyItemRangeChanged(position, count, payload);
    }
}

源碼實現(xiàn)很簡單,總結(jié)一下:

  • 首先排除新舊數(shù)據(jù)為空的情況乳蛾,這種情況不需要計算diff
  • 在子線程中計算DiffResult暗赶,在主線程將DiffResult設(shè)置給Adapter,解決主線程阻塞問題
  • 將Adapter的數(shù)據(jù)代理給AsyncListDiffer肃叶,解決Adapter與DiffUtil的數(shù)據(jù)一致性問題

完結(jié)蹂随,撒花
喜歡這篇文章記得給我一個小心心哦

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市因惭,隨后出現(xiàn)的幾起案子岳锁,更是在濱河造成了極大的恐慌,老刑警劉巖蹦魔,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件激率,死亡現(xiàn)場離奇詭異,居然都是意外死亡勿决,警方通過查閱死者的電腦和手機(jī)乒躺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來低缩,“玉大人嘉冒,你說我怎么就攤上這事。” “怎么了健爬?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵控乾,是天一觀的道長。 經(jīng)常有香客問我娜遵,道長蜕衡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任设拟,我火速辦了婚禮慨仿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘纳胧。我一直安慰自己镰吆,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布跑慕。 她就那樣靜靜地躺著万皿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪核行。 梳的紋絲不亂的頭發(fā)上牢硅,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機(jī)與錄音芝雪,去河邊找鬼减余。 笑死,一個胖子當(dāng)著我的面吹牛惩系,可吹牛的內(nèi)容都是我干的位岔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼堡牡,長吁一口氣:“原來是場噩夢啊……” “哼抒抬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起悴侵,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤瞧剖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后可免,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體抓于,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡裸弦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年窿春,在試婚紗的時候發(fā)現(xiàn)自己被綠了歹嘹。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吧兔。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡戳气,死狀恐怖驳概,靈堂內(nèi)的尸體忽然破棺而出若河,到底是詐尸還是另有隱情平道,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布灼舍,位于F島的核電站吼和,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏骑素。R本人自食惡果不足惜炫乓,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望献丑。 院中可真熱鬧末捣,春花似錦、人聲如沸创橄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽妥畏。三九已至邦邦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間醉蚁,已是汗流浹背圃酵。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留馍管,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓薪韩,卻偏偏與公主長得像确沸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子俘陷,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容