奶牛刀

Android——DiffUtil

2019-08-23  本文已影响0人  四喜汤圆

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

一、作用

可以计算两个 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

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实现单选列表的优雅之路.

上一篇 下一篇

猜你喜欢

热点阅读