美文网首页奶牛刀
Android——DiffUtil

Android——DiffUtil

作者: 四喜汤圆 | 来源:发表于2019-08-23 00:21 被阅读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

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

    相关文章

      网友评论

        本文标题:Android——DiffUtil

        本文链接:https://www.haomeiwen.com/subject/yjwbsctx.html