Android——DiffUtil

閱讀了大神寫的代碼,才知道每一行都不是白寫的,寫的有理有據(jù),還很優(yōu)雅。膜拜....

一、作用

可以計算兩個 List 之間的差異,得到兩個 List 之間的差異集,如果 List 集合很大,計算兩個 List 之間的差異耗時,應該放到子線程中執(zhí)行,計算得到 DiffUtil.DiffResult 后,將該結(jié)果集應用到主線程的 RecyclerView 上。

二、相關(guān)概念

1. 相關(guān)類

(1)DiffUtil.Callback

計算兩個 List 之間的差異時,由 DiffUtil 調(diào)用,

(2)DiffUtil.ItemCallback

用于計算 List 中兩個 non-null Item 的差異

(3)DiffUtil.DiffResult

保存了DiffUtil.calculateDiff(callback,boolean)的返回結(jié)果

2. 相關(guān)方法

(1)static DiffUtil.calculateDiff(DiffUtil.Callback cb)

(2)static DiffUtil.calculateDiff(DiffUtil.Callback cb,boolean detectMoves)

如果 old 和 new List 以相同的規(guī)則進行過排序,并且 Item 從不會移動(改變位置),那么,可以禁用 detectMoves=false,提高計算效率

三、使用

1. Item 實體類

項目中使用這個的場景可能就是:老數(shù)據(jù)已經(jīng)填充好了 Adapter,這時又從網(wǎng)絡中拉取了新數(shù)據(jù),那么使用 DiffUtil 比較兩個數(shù)據(jù)集的差異,將修改應用到 Adapter。此處為了復用舊數(shù)據(jù)源模擬新的數(shù)據(jù)集,所以為其實現(xiàn)Clonable接口

public class User implements Cloneable {

    private int id;
    private String name;
    private int age;
    private String profile;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getProfile() {
        return profile;
    }

    public void setProfile(String profile) {
        this.profile = profile;
    }

    public User(int id, String name, int age, String profile) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.profile = profile;
    }

    @NonNull
    @Override
    public User clone() {
        User o = null;
        try {
            o = (User) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return o;
    }
}

2. 實現(xiàn)一個普通的 Adapter

  • 繼承RecyclerView.Adapter,實現(xiàn)相關(guān)的抽象方法

  • 創(chuàng)建 ViewHolder,繼承自RecyclerView.ViewHolder

  • 在 Adapter 中保存數(shù)據(jù)源、上下文等

public class MyDiffAdapter extends RecyclerView.Adapter < MyDiffAdapter.MyTicketViewHolder > {

    private List < User > mData;
    private Context mContext;
    private LayoutInflater mLayoutInflater;

    public MyDiffAdapter(List < User > data, Context context) {
        mData = data;
        mContext = context;
        mLayoutInflater = LayoutInflater.from(context);
    }

    public List < User > getData() {
        return mData;
    }

    public void setData(List < User > data) {
        mData = data;
    }

    @NonNull
    @Override
    public MyTicketViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = mLayoutInflater.inflate(R.layout.user_item, parent, false);
        return new MyTicketViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull MyTicketViewHolder holder, int position) {
        User user = mData.get(position);
        // 為控件綁定數(shù)據(jù)
    }

    @Override
    public int getItemCount() {
        return mData == null ? 0 : mData.size();
    }

    class MyTicketViewHolder extends RecyclerView.ViewHolder {
        public MyTicketViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }

}

3. 為 Adapter 設置好初始數(shù)據(jù)源,先讓它跑起來哈~

設置數(shù)據(jù)集時可以先進行排序,防止顯示亂序

private void initViews() {
    mRecyclerView = findViewById(R.id.user_rv);
    mRefreshBtn = findViewById(R.id.btn_refresh);

    mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
    // 1,創(chuàng)建Adapter
    List < User > data = initData();
    mAdapter = new MyDiffAdapter(data, this);
    // 2,為RecyclerView設置適配器
    mRecyclerView.setAdapter(mAdapter);
}

private List < User > initData() {
    List < User > data = new ArrayList < > ();
    data.add(new User(1, "福子", 10, "adfada"));
    data.add(new User(2, "大牛", 10, "adfada"));
    data.add(new User(1, "栓子", 10, "adfada"));
    data.add(new User(4, "鐵柱", 10, "adfada"));
    data.add(new User(5, "鋼蛋", 10, "adfada"));
    return data;
}

4. DiffUtil 的簡單使用

模擬從網(wǎng)絡加載新的數(shù)據(jù)源,然后設置給 Adapter。

創(chuàng)建自己的 DiffUtil.Callback,定義自己的 Item 比較規(guī)則。

public class MyDiffCallback extends DiffUtil.Callback {

    private List < User > oldData;
    private List < User > newData;

    // 這里通過構(gòu)造函數(shù)把新老數(shù)據(jù)集傳進來
    public MyDiffCallback(List < User > oldData, List < User > newData) {
        this.oldData = oldData;
        this.newData = newData;
    }

    @Override
    public int getOldListSize() {
        return oldData == null ? 0 : oldData.size();
    }

    @Override
    public int getNewListSize() {
        return newData == null ? 0 : newData.size();
    }

    // 判斷是不是同一個Item:如果Item有唯一標志的Id的話,建議此處判斷id
    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        User oldUser = oldData.get(oldItemPosition);
        User newUser = newData.get(newItemPosition);
        return oldUser.getId() == newUser.getId();
    }

    // 判斷兩個Item的內(nèi)容是否相同
    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        // 默認內(nèi)容是相同的,只要有一項不同,則返回false
        User oldUser = oldData.get(oldItemPosition);
        User newUser = newData.get(newItemPosition);
        // name
        if (!oldUser.getName().equals(newUser.getName())) {
            return false;
        }
        // age
        if (oldUser.getAge() != newUser.getAge()) {
            return false;
        }
        // profile
        if (!oldUser.getProfile().equals(newUser.getProfile())) {
            return false;
        }
        return true;
    }
}

此處添加一個按鈕,模擬從網(wǎng)絡上獲取數(shù)據(jù)后刷新列表的操作。利用 DiffUtil 計算新老數(shù)據(jù)集的差異,并將差異應用到 Adapter 上。

private void initListener() {
    mRefreshBtn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            refreshData();
        }
    });
}
private void refreshData() {
    // 新的數(shù)據(jù)源
    List < User > oldData = mAdapter.getData();
    List < User > newData = new ArrayList < > ();
    for (int i = 0; i < oldData.size(); i++) {
        newData.add(oldData.get(i).clone());
    }
    // 模擬新增數(shù)據(jù)
    newData.add(new User(6, "趙子龍", 100, "一個神人"));
    // 模擬數(shù)據(jù)修改
    newData.get(0).setName("福子222");
    newData.get(0).setProfile("這是一個有福的女子");
    // 模擬數(shù)據(jù)移位
    User user = newData.get(1);
    newData.remove(user);
    newData.add(user);

    // 1,首先將新數(shù)據(jù)集設置給Adapter
    mAdapter.setData(newData);
    // 2,計算新老數(shù)據(jù)集差異,將差異更新到Adapter
    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffCallback(oldData,newData));
    diffResult.dispatchUpdatesTo(mAdapter);
}

此處 DiffUtil 計算新老數(shù)據(jù)集的差異,然后根據(jù)差異自動調(diào)用以下4個方法,實現(xiàn) Item 的定向刷新。

adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
adapter.notifyItemRangeChanged(position, count, payload);

注意:要記得先把新的數(shù)據(jù)源設置給 Adapter,然后將新老數(shù)據(jù)集的差異更新到 Adapter。因為 Adapter 更新數(shù)據(jù)時可能會用到新數(shù)據(jù)集中的數(shù)據(jù)(這個后面的高級用法中會提到)。

// 1,首先將新數(shù)據(jù)集設置給Adapter
mAdapter.setData(newData);
// 2,計算新老數(shù)據(jù)集差異,將差異更新到Adapter
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffCallback());
diffResult.dispatchUpdatesTo(mAdapter);

缺點:例如newData.get(0).setName("福子222"); newData.get(0).setProfile("這是一個有福的女子");中,我明明只想修改2個字段的值,卻給我刷新了整個 Item 。所以還是有改進空間的,下面實現(xiàn)RecyclerView 的部分綁定。

5. DiffUtil 的高級用法——整個數(shù)據(jù)源發(fā)生改變時的部分綁定

雖然數(shù)據(jù)源發(fā)生改變了,但還是可以做到部分綁定,只更新個別控件。核心思想:重寫 DiffUtil.Callback 中的public Object getChangePayload(int oldItemPosition, int newItemPosition)方法,并配合 Adapter 中3個參數(shù)的public void onBindViewHolder(@NonNull MyTicketViewHolder holder, int position, @NonNull List<Object> payloads)

DiffUtil.Callback 中重寫getChangePayload()方法

public static final String KEY_NAME = "name";
public static final String KEY_AGE = "age";
public static final String KEY_PROFILE = "profile";

@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
    User oldUser = oldData.get(oldItemPosition);
    User newUser = newData.get(newItemPosition);
    // 這里就不用比較核心字段 id 了,因為id不相同也不可能走到這一步
    Bundle payload = new Bundle();
    // name
    if (!oldUser.getName().equals(newUser.getName())) {
        payload.putString(KEY_NAME, newUser.getName());
    }
    // age
    if (oldUser.getAge() != newUser.getAge()) {
        payload.putInt(KEY_AGE, newUser.getAge());
    }
    // profile
    if (!oldUser.getProfile().equals(newUser.getProfile())) {
        payload.putString(KEY_PROFILE, newUser.getProfile());
    }
    if (payload.size() == 0) {
        // 如果沒有變化就傳空
        return null;
    }
    return payload;
}

Adapter 中重寫onBindViewHolder(),完成助攻。

@Override
public void onBindViewHolder(@NonNull MyTicketViewHolder holder, int position, @NonNull List < Object > payloads) {
    // payload 不會為null,但可能為empty
    if (payloads.isEmpty()) {
        // 如果payload是空的,那就進行一次 full bind
        onBindViewHolder(holder, position);
    } else {
        Bundle bundle = (Bundle) payloads.get(0);
        User user = mData.get(position);
        for (String key: bundle.keySet()) {
            switch (key) {
                case KEY_NAME:
                    // 局部更新名字:這里可以用 payload 里面的數(shù)據(jù),不過 mData 中的數(shù)據(jù)也是新的,也可以用
                    holder.nameTv.setText(user.getName());
                    break;
                case KEY_AGE:
                    holder.ageTv.setText(user.getAge() + "");
                    break;
                case KEY_PROFILE:
                    holder.profileTv.setText(user.getProfile());
                    break;
                default:
                    break;

            }
        }
    }
}

6. DiffUtil 的高級用法——明確已知某個 Item 發(fā)生改變時的部分綁定

上面說的是整個數(shù)據(jù)源發(fā)生變化了該怎么做實現(xiàn)部分綁定,但如果我明確的知道某個 position 的 item 發(fā)生了改變的話,不可能重新構(gòu)造個數(shù)據(jù)源進行刷新吧,別急且聽下文分解。

核心是:首先更新被選中 Item 的數(shù)據(jù)源,然后把修改的內(nèi)容放到 payload 中,調(diào)用notifyItemChange()方法更新 Item 時把 payload 傳入,接下來會回調(diào)到public void onBindViewHolder(@NonNull MyTicketViewHolder holder, int position, @NonNull List<Object> payloads)中,實現(xiàn)部分綁定。

// item 點擊事件:假設點擊以后name會變
private void onItemClick(int position) {
    // 1,更新item的數(shù)據(jù)源
    User user = mAdapter.getData().get(position);
    String newName = "新的張無忌";
    user.setName(newName);
    // 2, 傳遞一個 payload
    Bundle payload = new Bundle();
    payload.putString(KEY_NAME, newName);
    mAdapter.notifyItemChanged(position, payload);
}

四、原理

三中5、6對整個數(shù)據(jù)源/單個 item 進行局部刷新,是有原理可追尋的。

1. DiffUtil 的高級用法——整個數(shù)據(jù)源發(fā)生改變時的部分綁定

(1)diffResult.dispatchUpatesTo(mAdaptetr)

DiffUtil.DiffResult.dispatchUpdatesTo(@NonNull final RecyclerView.Adapter adapter)

/**
 * 將更新事件分發(fā)到給定的Adapter
 * <p>
 * 例如:你有一個{@link RecyclerView.Adapter Adapter},這個Adapter有一個{@link List}數(shù)據(jù)源
 * 你可以先將新的數(shù)據(jù)源賦給Adapter,然后調(diào)用該發(fā)方法將所有更新分發(fā)到RecyclerView
 * <pre>
 *     List oldList = mAdapter.getData();
 *     DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldList, newList));
 *     mAdapter.setData(newList);
 *     result.dispatchUpdatesTo(mAdapter);
 * </pre>
 * <p>
 * 注意:RecyclerView要求在你更改數(shù)據(jù)源后立即將更新分發(fā)到Adapter Note that the RecyclerView requires you to dispatch adapter updates immediately when you
 * <p>
 * @param adapter :適配器,正在顯示舊數(shù)據(jù),即將顯示新數(shù)據(jù)。
 * @see AdapterListUpdateCallback
 */
public void dispatchUpdatesTo(@NonNull final RecyclerView.Adapter adapter) {
    dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
}

AdapterListUpdateCallback.class

/**
 * ListUpdateCallback that dispatches update events to the given adapter.
 * 將更新事件分發(fā)給給定 Adapter
 * @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter)
 */
public final class AdapterListUpdateCallback implements ListUpdateCallback {
    @NonNull
    private final RecyclerView.Adapter mAdapter;

    /**
     * Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter.
     *
     * @param adapter The Adapter to send updates to.
     */
    public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
        mAdapter = adapter;
    }

    /**
     * Called when {@code count} number of items are inserted at the given position.
     * 當在position位置插入count個Item時調(diào)用
     * @param position The position of the new item.
     * @param count    The number of items that have been added.
     */
    @Override
    public void onInserted(int position, int count) {
        mAdapter.notifyItemRangeInserted(position, count);
    }

    /**
     * Called when {@code count} number of items are removed from the given position.
     *position位置的count個Item被刪除
     * @param position The position of the item which has been removed.
     * @param count    The number of items which have been removed.
     */
    @Override
    public void onRemoved(int position, int count) {
        mAdapter.notifyItemRangeRemoved(position, count);
    }

    /**
     * Called when an item changes its position in the list.
     * 當一個item改變了它的position時調(diào)用
     * @param fromPosition The previous position of the item before the move.
     * @param toPosition   The new position of the item.
     */
    @Override
    public void onMoved(int fromPosition, int toPosition) {
        mAdapter.notifyItemMoved(fromPosition, toPosition);
    }

    /**
     * Called when {@code count} number of items are updated at the given position.
     *  當position位置的item內(nèi)容發(fā)生改變時調(diào)用
     * @param position The position of the item which has been updated.
     * @param count    The number of items which has changed.
     */
    @Override
    public void onChanged(int position, int count, Object payload) {
        mAdapter.notifyItemRangeChanged(position, count, payload);
    }
}

(2)public void dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback)

/**
         * Dispatches update operations to the given Callback.
         將更新操作分派給指定的callback
         * <p>
         這些更新是原子性的,例如:第一個的更新會影響后面的更新
         * These updates are atomic such that the first update call affects every update call that
         * comes after it (the same as RecyclerView).
         *
         * @param updateCallback The callback to receive the update operations.
         * @see #dispatchUpdatesTo(RecyclerView.Adapter)
         */
public void dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback) {

}

在該方法中計算出 item 的增刪改移動,然后將更新分配給指定的 callback,調(diào)用 AdapterListUpdateCallback 中對應的4個方法這個4個方法又最終會調(diào)用到onBindViewHolder()中。

2. DiffUtil 的高級用法——整個數(shù)據(jù)源發(fā)生改變時的部分綁定

AdapterListUpdateCallback 類中的onItemRangeChanged

public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
    // fallback to onItemRangeChanged(positionStart, itemCount) if app
    // does not override this method.,如果使用者沒有重寫該方法時,默認調(diào)用不帶payload的2個參數(shù)方法
    onItemRangeChanged(positionStart, itemCount);
}

onBindViewHolder()

/**
 * Called by RecyclerView to display the data at the specified position. This method
 * should update the contents of the {@link ViewHolder#itemView} to reflect the item at
 * the given position.
 * <p>
 * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method
 * again if the position of the item changes in the data set unless the item itself is
 * invalidated or the new position cannot be determined. For this reason, you should only
 * use the <code>position</code> parameter while acquiring the related data item inside
 * this method and should not keep a copy of it. If you need the position of an item later
 * on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will
 * have the updated adapter position.
 * <p>
 * Partial bind vs full bind:
 * <p>
 * The payloads parameter is a merge list from {@link #notifyItemChanged(int, Object)} or
 * {@link #notifyItemRangeChanged(int, int, Object)}.  If the payloads list is not empty,
 * the ViewHolder is currently bound to old data and Adapter may run an efficient partial
 * update using the payload info.  If the payload is empty,  Adapter must run a full bind.
 * Adapter should not assume that the payload passed in notify methods will be received by
 * onBindViewHolder().  For example when the view is not attached to the screen, the
 * payload in notifyItemChange() will be simply dropped.
 *
 * @param holder The ViewHolder which should be updated to represent the contents of the
 *               item at the given position in the data set.
 * @param position The position of the item within the adapter's data set.
 * @param payloads A non-null list of merged payloads. Can be empty list if requires full
 *                 update.
 */
public void onBindViewHolder(@NonNull VH holder, int position,
    @NonNull List < Object > payloads) {
    onBindViewHolder(holder, position);
}

Android】RecyclerView的好伴侶:詳解DiffUtil
【Android】 RecyclerView、ListView實現(xiàn)單選列表的優(yōu)雅之路.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

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