android-RecyclerView的DiffUtil差异化工具使用

没有效果图的示例简直就是扯淡

在这里插入图片描述

有点模糊,大家凑活看吧。。。

DiffUtil是什么?

DiffUtil是一个工具类,当你的RecyclerView需要更新数据时,将新旧数据集传给它,它就能快速告知adapter有哪些数据需要更新。就相当于如果改变了就对某个item刷新,没改变就没刷新,可以简称为局部刷新。

DiffUtil 的优势

我在最初接触 DiffUtil 时, 心中便对它有颇多的好感, 包括:

算法听提来就很nb, 一定是个好东西;
简化了 RecyclerView 的刷新逻辑, 无须关心该调用 notifyItemInserted 还是 notifyItemChanged, 一律submitList 就完事了(虽然 notifyDataSetChanged 也能做到, 但是性能拉胯, 而且没有动画);
LiveData 或者 Flow 监听单一 List 数据源时, 往往很难知道, 整个 List 中到底哪些数据项被更新了, 只能调用notifyDataSetChanged 方法, 而 DiffUtil 恰好就能解决这个问题, 无脑 submitList 就完事了.

总的来说,尽量使用diffutil,可以更好的去刷新数据,速度更快,性能更好,效果更棒。

好了不说了,上代码吧

核心类:DiffCallBack.class

import android.os.Bundle;

import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;

import java.util.List;

/**
 * cc--核心类 用来判断 新旧Item是否相等
 * cc 96.10.11
 */
public class DiffCallBack extends DiffUtil.Callback {

    //旧数据集合
    private List<TestBean> mOldData;
    //新数据集合
    private List<TestBean> mNewData;

    /**
     * 拿到两个集合
     * @param mOldData
     * @param mNewData
     */
    public DiffCallBack(List<TestBean> mOldData, List<TestBean> mNewData) {
        this.mOldData = mOldData;
        this.mNewData = mNewData;
    }

    /**
     * 旧数据集合
     * @return
     */
    @Override
    public int getOldListSize() {
        return mOldData != null ? mOldData.size() : 0;
    }

    /**
     * 新数据集合
     * @return
     */
    @Override
    public int getNewListSize() {
        return mNewData != null ? mNewData.size() : 0;
    }

    /**
     * Called by the DiffUtil to decide whether two object represent the same Item.
     * 被DiffUtil调用,用来判断 两个对象是否是相同的Item。
     * For example, if your items have unique ids, this method should check their id equality.
     * 例如,如果你的Item有唯一的id字段,这个方法就 判断id是否相等。
     * 本例判断name字段是否一致
     *
     * @param oldItemPosition The position of the item in the old list
     * @param newItemPosition The position of the item in the new list
     * @return True if the two items represent the same object or false if they are different.
     */
    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return mOldData.get(oldItemPosition).getName().equals(mNewData.get(newItemPosition).getName());
    }

    /**
     * Called by the DiffUtil when it wants to check whether two items have the same data.
     * 被DiffUtil调用,用来检查 两个item是否含有相同的数据
     * DiffUtil uses this information to detect if the contents of an item has changed.
     * DiffUtil用返回的信息(true false)来检测当前item的内容是否发生了变化
     * DiffUtil uses this method to check equality instead of {@link Object#equals(Object)}
     * DiffUtil 用这个方法替代equals方法去检查是否相等。
     * so that you can change its behavior depending on your UI.
     * 所以你可以根据你的UI去改变它的返回值
     * For example, if you are using DiffUtil with a
     * {@link androidx.recyclerview.widget.RecyclerView.Adapter RecyclerView.Adapter}, you should
     * return whether the items' visual representations are the same.
     * 例如,如果你用RecyclerView.Adapter 配合DiffUtil使用,你需要返回Item的视觉表现是否相同。
     * This method is called only if {@link #areItemsTheSame(int, int)} returns
     * {@code true} for these items.
     * 这个方法仅仅在areItemsTheSame()返回true时,才调用。
     *
     * @param oldItemPosition The position of the item in the old list
     * @param newItemPosition The position of the item in the new list which replaces the
     *                        oldItem
     * @return True if the contents of the items are the same or false if they are different.
     */
    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        TestBean beanOld = mOldData.get(oldItemPosition);
        TestBean beanNew = mNewData.get(newItemPosition);
        if (!beanOld.getContent().equals(beanNew.getContent())) {
            return false;//如果有内容不同,就返回false
        }
        if (beanOld.getPic() != beanNew.getPic()) {
            return false;//如果有内容不同,就返回false
        }
        if (beanOld.getLookNumber() != beanNew.getLookNumber()) {
            return false;//如果有内容不同,就返回false
        }
        if (beanOld.getCommentNumber() != beanNew.getCommentNumber()) {
            return false;//如果有内容不同,就返回false
        }
        return true; //默认两个data内容是相同的
    }

    /**
     * When {@link #areItemsTheSame(int, int)} returns {@code true} for two items and
     * {@link #areContentsTheSame(int, int)} returns false for them, DiffUtil
     * calls this method to get a payload about the change.
     * <p>
     * 当{@link #areItemsTheSame(int, int)} 返回true,且{@link #areContentsTheSame(int, int)} 返回false时,DiffUtils会回调此方法,
     * 去得到这个Item(有哪些)改变的payload。
     * <p>
     * For example, if you are using DiffUtil with {@link androidx.recyclerview.widget.RecyclerView}, you can return the
     * particular field that changed in the item and your
     * {@link androidx.recyclerview.widget.RecyclerView.ItemAnimator ItemAnimator} can use that
     * information to run the correct animation.
     * <p>
     * 例如,如果你用RecyclerView配合DiffUtils,你可以返回  这个Item改变的那些字段,
     * {@link androidx.recyclerview.widget.RecyclerView.ItemAnimator ItemAnimator} 可以用那些信息去执行正确的动画
     * <p>
     * Default implementation returns {@code null}.\
     * 默认的实现是返回null
     *
     * @param oldItemPosition The position of the item in the old list
     * @param newItemPosition The position of the item in the new list
     * @return A payload object that represents the change between the two items.
     * 返回 一个 代表着新老item的改变内容的 payload对象,
     */
    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        //实现这个方法 就能成为文艺青年中的文艺青年
        // 定向刷新中的部分更新
        // 效率最高
        //只是没有了ItemChange的白光一闪动画,(反正我也觉得不太重要)
        TestBean oldBean = mOldData.get(oldItemPosition);
        TestBean newBean = mNewData.get(newItemPosition);

        //这里就不用比较核心字段了,一定相等
        Bundle payload = new Bundle();
        if (!oldBean.getContent().equals(newBean.getContent())) {
            payload.putString("KEY_DESC", newBean.getContent());
        }
        if (oldBean.getPic() != newBean.getPic()) {
            payload.putInt("KEY_PIC", newBean.getPic());
        }
        if (oldBean.getLookNumber() != newBean.getLookNumber()) {
            payload.putInt("KEY_LOOK_NUMBER", newBean.getLookNumber());
        }
        if (oldBean.getCommentNumber() != newBean.getCommentNumber()) {
            payload.putInt("KEY_COMMENT_NUMBER", newBean.getCommentNumber());
        }

        if (payload.size() == 0)//如果没有变化 就传空
            return null;
        return payload;//
    }
}

DiffAdapter.class

import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.recyclerview.widget.RecyclerView;

import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.bumptech.glide.request.RequestOptions;
import com.mcxtzhang.diffutils.R;

import java.util.List;

/**
 * 介绍:普普通的adapter,
 * 但是 唯一亮点~
 * public void onBindViewHolder(DiffVH holder, int position, List<Object> payloads)
 * 重写这个方法
 * cc--
 * cc 96.10.11
 */
public class DiffAdapter extends RecyclerView.Adapter<DiffAdapter.DiffVH> {
    private final static String TAG = "zxt";
    private List<TestBean> mDatas;
    private Context mContext;
    private LayoutInflater mInflater;

    public DiffAdapter(Context mContext, List<TestBean> mDatas) {
        this.mContext = mContext;
        this.mDatas = mDatas;
        mInflater = LayoutInflater.from(mContext);
    }

    public void setDatas(List<TestBean> mDatas) {
        this.mDatas = mDatas;
    }

    @Override
    public DiffVH onCreateViewHolder(ViewGroup parent, int viewType) {
        return new DiffVH(mInflater.inflate(R.layout.item_diff, parent, false));
    }

    @Override
    public void onBindViewHolder(final DiffVH holder, final int position) {
        TestBean bean = mDatas.get(position);

        holder.mTvName.setText(bean.getName());
        holder.mTvContent.setText(bean.getContent());
        displayCircleImage(mContext, bean.getPic(), holder.mImgUrl, 0);

        holder.mTvLookNumber.setText(new StringBuffer("浏览量:").append(bean.getLookNumber()));
        holder.mTvCommentNumber.setText(new StringBuffer("评论量:").append(bean.getCommentNumber()));
    }

    @Override
    public void onBindViewHolder(DiffVH holder, int position, List<Object> payloads) {
        if (payloads.isEmpty()) {
            onBindViewHolder(holder, position);
        } else {
            //文艺青年中的文青
            Bundle payload = (Bundle) payloads.get(0);//取出我们在getChangePayload()方法返回的bundle
            TestBean bean = mDatas.get(position);//取出新数据源,(可以不用)
            for (String key : payload.keySet()) {
                switch (key) {
                    case "KEY_DESC":
                        //这里可以用payload里的数据,不过data也是新的 也可以用
                        holder.mTvContent.setText(bean.getContent());
                        break;
                    case "KEY_PIC":
                        displayCircleImage(mContext, payload.getInt(key), holder.mImgUrl, 0);
                        break;
                    case "KEY_LOOK_NUMBER":
                        holder.mTvLookNumber.setText(new StringBuffer("浏览量:").append(payload.getInt(key)));
                        break;
                    case "KEY_COMMENT_NUMBER":
                        holder.mTvCommentNumber.setText(new StringBuffer("评论量:").append(payload.getInt(key)));
                        break;
                    default:
                        break;
                }
            }
        }
    }

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

    class DiffVH extends RecyclerView.ViewHolder {

        ImageView mImgUrl;
        TextView mTvName, mTvContent, mTvLookNumber, mTvCommentNumber;

        public DiffVH(View itemView) {
            super(itemView);

            mImgUrl = itemView.findViewById(R.id.img_url);

            mTvName = itemView.findViewById(R.id.txt_name);
            mTvContent = itemView.findViewById(R.id.txt_content);
            mTvLookNumber = itemView.findViewById(R.id.tv_look_number);
            mTvCommentNumber = itemView.findViewById(R.id.tv_comment_number);
        }
    }

    public static void displayCircleImage(Context context, int url, ImageView imageView, int placeHolderId) {
        RequestOptions options = (new RequestOptions()).circleCrop().placeholder(placeHolderId);
        Glide.with(context).load(url).transition(DrawableTransitionOptions.withCrossFade()).apply(options).into(imageView);
    }

}

TestBean.class

/**
 * 介绍:一个普通的JavaBean,但是实现了clone方法,仅仅用于写Demo时,模拟刷新从网络获取数据用,
 * 因为使用DiffUtils比较新老数据集差异时,会遍历新老数据集的每个data,要确保他们的内存地址(指针)不一样,否则比较的是新老data是同一个,就一定相同,
 * 实际项目不需要,因为刷新时,数据一般从网络拉取,并且用Gson等解析出来,内存地址一定是不一样的。
 * cc--
 * cc 96.10.11
 */
public class TestBean implements Cloneable {
    private String name;
    private String content;
    private int pic;
    private int lookNumber;
    private int commentNumber;

    public TestBean(String name, String content, int pic, int lookNumber, int commentNumber) {
        this.name = name;
        this.content = content;
        this.pic = pic;
        this.lookNumber = lookNumber;
        this.commentNumber = commentNumber;
    }

    public int getLookNumber() {
        return lookNumber;
    }

    public void setLookNumber(int lookNumber) {
        this.lookNumber = lookNumber;
    }

    public int getCommentNumber() {
        return commentNumber;
    }

    public void setCommentNumber(int commentNumber) {
        this.commentNumber = commentNumber;
    }

    public int getPic() {
        return pic;
    }

    public void setPic(int pic) {
        this.pic = pic;
    }

    public String getName() {
        return name;
    }

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

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    //仅写DEMO 用 实现克隆方法
    @Override
    public TestBean clone() throws CloneNotSupportedException {
        TestBean bean = null;
        try {
            bean = (TestBean) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return bean;
    }
}

MainActivity.class

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.mcxtzhang.diffutils.R;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private List<TestBean> mDatas;
    private RecyclerView mRv;
    private DiffAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();
        mRv = (RecyclerView) findViewById(R.id.rv);
        mRv.setLayoutManager(new LinearLayoutManager(this));
        mAdapter = new DiffAdapter(this, mDatas);
        mRv.setAdapter(mAdapter);
    }

    private void initData() {
        mDatas = new ArrayList<>();
        mDatas.add(new TestBean("吴大哥", "我的内容是一样的。。。", R.drawable.pic1, 34456, 865));
        mDatas.add(new TestBean("吴大哥", "我的内容是一样的。。。", R.drawable.pic2, 2345, 98765));
        mDatas.add(new TestBean("吴大哥", "我的内容是一样的。。。", R.drawable.pic3, 87, 45));
        mDatas.add(new TestBean("吴大哥", "我的内容是一样的。。。", R.drawable.pic5, 2368435, 7587));
    }

    /**
     * 模拟刷新操作
     *
     * @param view
     */
    public void onRefresh(View view) {
        try {
            mNewDatas = new ArrayList<>();
            for (TestBean bean : mDatas) {
                mNewDatas.add(bean.clone());//clone一遍旧数据 ,模拟刷新操作
            }
            mNewDatas.get(0).setLookNumber((int) (Math.random() * 200));
            mNewDatas.get(0).setCommentNumber((int) (Math.random() * 200));
            mNewDatas.get(1).setLookNumber((int) (Math.random() * 200));
            mNewDatas.get(1).setCommentNumber((int) (Math.random() * 200));
            mNewDatas.get(2).setLookNumber((int) (Math.random() * 200));
            mNewDatas.get(2).setCommentNumber((int) (Math.random() * 200));
            mNewDatas.get(3).setLookNumber((int) (Math.random() * 200));
            mNewDatas.get(3).setCommentNumber((int) (Math.random() * 200));

            //新宠
            //利用DiffUtil.calculateDiff()方法,传入一个规则DiffUtil.Callback对象,和是否检测移动item的 boolean变量,得到DiffUtil.DiffResult 的对象
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //放在子线程中计算DiffResult
                    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, mNewDatas), true);
                    Message message = mHandler.obtainMessage(1);
                    message.obj = diffResult;//obj存放DiffResult
                    message.sendToTarget();
                }
            }).start();
            //mAdapter.notifyDataSetChanged();//以前普通青年的我们只能这样,现在我们是文艺青年了,有新宠了

        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }

    private List<TestBean> mNewDatas;//增加一个变量暂存newList
    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        @SuppressLint("HandlerLeak")
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    //取出Result
                    DiffUtil.DiffResult diffResult = (DiffUtil.DiffResult) msg.obj;
                    //利用DiffUtil.DiffResult对象的dispatchUpdatesTo()方法,传入RecyclerView的Adapter,轻松成为文艺青年
                    diffResult.dispatchUpdatesTo(mAdapter);
                    //别忘了将新数据给Adapter
                    mDatas = mNewDatas;
                    mAdapter.setDatas(mDatas);
                    break;
            }
        }
    };

}

item_diff.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:padding="20dp"
    android:background="@color/colorAccent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/img_url"
        tools:src="@mipmap/ic_launcher"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:scaleType="centerCrop"
        tools:ignore="MissingConstraints" />

    <TextView
        android:id="@+id/txt_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toEndOf="@+id/img_url"
        android:layout_marginStart="10dp"
        android:textColor="#ffffff"
        tools:text="第一个"
        tools:ignore="MissingConstraints" />

    <TextView
        android:id="@+id/txt_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:textColor="#ffffff"
        app:layout_constraintTop_toBottomOf="@+id/img_url"
        tools:text="我的存在只为了证明定向刷新中的定向刷新"
        tools:ignore="MissingConstraints" />

    <TextView
        android:id="@+id/tv_look_number"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        tools:text="阅读量:100"
        android:padding="2dp"
        android:textColor="#ffffff"
        app:layout_constraintTop_toBottomOf="@+id/txt_content"
        tools:ignore="MissingConstraints" />

    <TextView
        android:id="@+id/tv_comment_number"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        tools:text="评论量:50"
        android:padding="2dp"
        android:textColor="#ffffff"
        android:layout_marginStart="30dp"
        app:layout_constraintTop_toBottomOf="@+id/txt_content"
        app:layout_constraintStart_toEndOf="@+id/tv_look_number"
        tools:ignore="MissingConstraints" />

</androidx.constraintlayout.widget.ConstraintLayout>
注:至此,所有的代码开发工作都已经结束了。

附上demo源码。

源码:源码请点这里

如果下不了源码,可以加微信,手机号在下面。


Q:486789970(QQ现在很少用)
V:18588400509(如果着急,可以直接加微信)
email:mr.cai_cai@foxmail.com
扫码加入微信群:在这里插入图片描述

如果有什么问题,欢迎大家指导。并相互联系,希望能够通过文章互相学习。

											                               	---财财亲笔
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谁抢我的小口口

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值