Android——DiffUtil

阅读了大神写的代码,才知道每一行都不是白写的,写的有理有据,还很优雅。膜拜....

一、作用

可以计算两个 List 之间的差异,得到两个 List 之间的差异集,如果 List 集合很大,计算两个 List 之间的差异耗时,应该放到子线程中执行,计算得到 DiffUtil.DiffResult 后,将该结果集应用到主线程的 RecyclerView 上。

二、相关概念

1. 相关类

(1)DiffUtil.Callback

计算两个 List 之间的差异时,由 DiffUtil 调用,

(2)DiffUtil.ItemCallback

用于计算 List 中两个 non-null Item 的差异

(3)DiffUtil.DiffResult

保存了DiffUtil.calculateDiff(callback,boolean)的返回结果

2. 相关方法

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

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

如果 old 和 new List 以相同的规则进行过排序,并且 Item 从不会移动(改变位置),那么,可以禁用 detectMoves=false,提高计算效率

三、使用

1. Item 实体类

项目中使用这个的场景可能就是:老数据已经填充好了 Adapter,这时又从网络中拉取了新数据,那么使用 DiffUtil 比较两个数据集的差异,将修改应用到 Adapter。此处为了复用旧数据源模拟新的数据集,所以为其实现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. 实现一个普通的 Adapter

  • 继承RecyclerView.Adapter,实现相关的抽象方法

  • 创建 ViewHolder,继承自RecyclerView.ViewHolder

  • 在 Adapter 中保存数据源、上下文等

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);
        // 为控件绑定数据
    }

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

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

}

3. 为 Adapter 设置好初始数据源,先让它跑起来哈~

设置数据集时可以先进行排序,防止显示乱序

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

    mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
    // 1,创建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 的简单使用

模拟从网络加载新的数据源,然后设置给 Adapter。

创建自己的 DiffUtil.Callback,定义自己的 Item 比较规则。

public class MyDiffCallback extends DiffUtil.Callback {

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

    // 这里通过构造函数把新老数据集传进来
    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的内容是否相同
    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        // 默认内容是相同的,只要有一项不同,则返回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;
    }
}

此处添加一个按钮,模拟从网络上获取数据后刷新列表的操作。利用 DiffUtil 计算新老数据集的差异,并将差异应用到 Adapter 上。

private void initListener() {
    mRefreshBtn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            refreshData();
        }
    });
}
private void refreshData() {
    // 新的数据源
    List < User > oldData = mAdapter.getData();
    List < User > newData = new ArrayList < > ();
    for (int i = 0; i < oldData.size(); i++) {
        newData.add(oldData.get(i).clone());
    }
    // 模拟新增数据
    newData.add(new User(6, "赵子龙", 100, "一个神人"));
    // 模拟数据修改
    newData.get(0).setName("福子222");
    newData.get(0).setProfile("这是一个有福的女子");
    // 模拟数据移位
    User user = newData.get(1);
    newData.remove(user);
    newData.add(user);

    // 1,首先将新数据集设置给Adapter
    mAdapter.setData(newData);
    // 2,计算新老数据集差异,将差异更新到Adapter
    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffCallback(oldData,newData));
    diffResult.dispatchUpdatesTo(mAdapter);
}

此处 DiffUtil 计算新老数据集的差异,然后根据差异自动调用以下4个方法,实现 Item 的定向刷新。

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

注意:要记得先把新的数据源设置给 Adapter,然后将新老数据集的差异更新到 Adapter。因为 Adapter 更新数据时可能会用到新数据集中的数据(这个后面的高级用法中会提到)。

// 1,首先将新数据集设置给Adapter
mAdapter.setData(newData);
// 2,计算新老数据集差异,将差异更新到Adapter
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffCallback());
diffResult.dispatchUpdatesTo(mAdapter);

缺点:例如newData.get(0).setName("福子222"); newData.get(0).setProfile("这是一个有福的女子");中,我明明只想修改2个字段的值,却给我刷新了整个 Item 。所以还是有改进空间的,下面实现RecyclerView 的部分绑定。

5. DiffUtil 的高级用法——整个数据源发生改变时的部分绑定

虽然数据源发生改变了,但还是可以做到部分绑定,只更新个别控件。核心思想:重写 DiffUtil.Callback 中的public Object getChangePayload(int oldItemPosition, int newItemPosition)方法,并配合 Adapter 中3个参数的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 里面的数据,不过 mData 中的数据也是新的,也可以用
                    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 发生改变时的部分绑定

上面说的是整个数据源发生变化了该怎么做实现部分绑定,但如果我明确的知道某个 position 的 item 发生了改变的话,不可能重新构造个数据源进行刷新吧,别急且听下文分解。

核心是:首先更新被选中 Item 的数据源,然后把修改的内容放到 payload 中,调用notifyItemChange()方法更新 Item 时把 payload 传入,接下来会回调到public void onBindViewHolder(@NonNull MyTicketViewHolder holder, int position, @NonNull List<Object> payloads)中,实现部分绑定。

// item 点击事件:假设点击以后name会变
private void onItemClick(int position) {
    // 1,更新item的数据源
    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对整个数据源/单个 item 进行局部刷新,是有原理可追寻的。

1. DiffUtil 的高级用法——整个数据源发生改变时的部分绑定

(1)diffResult.dispatchUpatesTo(mAdaptetr)

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

/**
 * 将更新事件分发到给定的Adapter
 * <p>
 * 例如:你有一个{@link RecyclerView.Adapter Adapter},这个Adapter有一个{@link List}数据源
 * 你可以先将新的数据源赋给Adapter,然后调用该发方法将所有更新分发到RecyclerView
 * <pre>
 *     List oldList = mAdapter.getData();
 *     DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldList, newList));
 *     mAdapter.setData(newList);
 *     result.dispatchUpdatesTo(mAdapter);
 * </pre>
 * <p>
 * 注意:RecyclerView要求在你更改数据源后立即将更新分发到Adapter Note that the RecyclerView requires you to dispatch adapter updates immediately when you
 * <p>
 * @param adapter :适配器,正在显示旧数据,即将显示新数据。
 * @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.
 * 将更新事件分发给给定 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时调用
     * @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时调用
     * @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内容发生改变时调用
     * @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,调用 AdapterListUpdateCallback 中对应的4个方法这个4个方法又最终会调用到onBindViewHolder()中。

2. DiffUtil 的高级用法——整个数据源发生改变时的部分绑定

AdapterListUpdateCallback 类中的onItemRangeChanged

public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
    // fallback to onItemRangeChanged(positionStart, itemCount) if app
    // does not override this method.,如果使用者没有重写该方法时,默认调用不带payload的2个参数方法
    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实现单选列表的优雅之路.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容