微信号:appjiagou

介绍:分享最有价值的APP技术干货文章,做一个有逼格的APP架构师,拒绝平庸,打造最有价值的APP社区!

详解7.0带来的新工具类:DiffUtil(上)

2016-12-25 15:16 APP架构师

一 概述

DiffUtil是support-v7:24.2.0中的新工具类,它用来比较两个数据集,寻找出旧数据集-》新数据集的最小变化量。 
说到数据集,相信大家知道它是和谁相关的了,就是我的最爱,RecyclerView。 
就我使用的这几天来看,它最大的用处就是在RecyclerView刷新时,不再无脑mAdapter.notifyDataSetChanged()。 
以前无脑mAdapter.notifyDataSetChanged()有两个缺点:

  1. 不会触发RecyclerView的动画(删除、新增、位移、change动画)

  2. 性能较低,毕竟是无脑的刷新了一遍整个RecyclerView , 极端情况下:新老数据集一模一样,效率是最低的。

使用DiffUtil后,改为如下代码:

 
           
  1. DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true);

  2. diffResult.dispatchUpdatesTo(mAdapter);

它会自动计算新老数据集的差异,并根据差异情况,自动调用以下四个方法

 
           
  1. adapter.notifyItemRangeInserted(position, count);

  2. adapter.notifyItemRangeRemoved(position, count);

  3. adapter.notifyItemMoved(fromPosition, toPosition);

  4. adapter.notifyItemRangeChanged(position, count, payload);

显然,这个四个方法在执行时都是伴有RecyclerView的动画的,且都是定向刷新方法,刷新效率蹭蹭的上升了。 
老规矩,先上图,

图一是无脑mAdapter.notifyDataSetChanged()的效果图,可以看到刷新交互很生硬,Item突然的出现在某个位置: 


图二是使用DiffUtils的效果图,最明显的是有插入、移动Item的动画: 


转成GIF有些渣,下载文末Demo运行效果更佳哦。

本文将包含且不仅包含以下内容:

1 先介绍DiffUtil的简单用法,实现刷新时的“增量更新”效果。(“增量更新”是我自己的叫法) 
2 DiffUtil的高级用法,在某项Item只有内容(data)变化,位置(position)未变化时,完成部分更新(官方称之为Partial bind,部分绑定)。 
3 了解到 RecyclerView.Adapter还有public void onBindViewHolder(VH holder, int position, List<Object> payloads)方法,并掌握它。 
4 在子线程中计算DiffResult,在主线程中刷新RecyclerView。 
5 少部分人不喜欢的notifyItemChanged()导致Item白光一闪的动画 如何去除。 
6 DiffUtil部分类、方法 官方注释的汉化


二 DiffUtil的简单用法

前文也提到,DiffUtil是帮助我们在刷新RecyclerView时,计算新老数据集的差异,并自动调用RecyclerView.Adapter的刷新方法,以完成高效刷新并伴有Item动画的效果。 
那么我们在学习它之前要先做一些准备工作,先写一个普通青年版,无脑notifyDataSetChanged()刷新的Demo。 
1 一个普通的JavaBean,但是实现了clone方法,仅用于写Demo模拟刷新用,实际项目不需要,因为刷新时,数据都是从网络拉取的。:

 
           
  1. class TestBean implements Cloneable {

  2.     private String name;

  3.     private String desc;

  4.     ....//get set方法省略

  5.     //仅写DEMO 用 实现克隆方法

  6.     @Override

  7.     public TestBean clone() throws CloneNotSupportedException {

  8.         TestBean bean = null;

  9.         try {

  10.             bean = (TestBean) super.clone();

  11.         } catch (CloneNotSupportedException e) {

  12.             e.printStackTrace();

  13.         }

  14.         return bean;

  15.     }

2 实现一个普普通通的RecyclerView.Adapter。

 
           
  1. public class DiffAdapter extends RecyclerView.Adapter<DiffAdapter.DiffVH> {

  2.     private final static String TAG = "zxt";

  3.     private List<TestBean> mDatas;

  4.     private Context mContext;

  5.     private LayoutInflater mInflater;

  6.  

  7.     public DiffAdapter(Context mContext, List<TestBean> mDatas) {

  8.         this.mContext = mContext;

  9.         this.mDatas = mDatas;

  10.         mInflater = LayoutInflater.from(mContext);

  11.     }

  12.  

  13.     public void setDatas(List<TestBean> mDatas) {

  14.         this.mDatas = mDatas;

  15.     }

  16.  

  17.     @Override

  18.     public DiffVH onCreateViewHolder(ViewGroup parent, int viewType) {

  19.         return new DiffVH(mInflater.inflate(R.layout.item_diff, parent, false));

  20.     }

  21.  

  22.     @Override

  23.     public void onBindViewHolder(final DiffVH holder, final int position) {

  24.         TestBean bean = mDatas.get(position);

  25.         holder.tv1.setText(bean.getName());

  26.         holder.tv2.setText(bean.getDesc());

  27.         holder.iv.setImageResource(bean.getPic());

  28.     }

  29.  

  30.     @Override

  31.     public int getItemCount() {

  32.         return mDatas != null ? mDatas.size() : 0;

  33.     }

  34.  

  35.     class DiffVH extends RecyclerView.ViewHolder {

  36.         TextView tv1, tv2;

  37.         ImageView iv;

  38.  

  39.         public DiffVH(View itemView) {

  40.             super(itemView);

  41.             tv1 = (TextView) itemView.findViewById(R.id.tv1);

  42.             tv2 = (TextView) itemView.findViewById(R.id.tv2);

  43.             iv = (ImageView) itemView.findViewById(R.id.iv);

  44.         }

  45.     }

  46. }

3 Activity代码:

 
           
  1. public class MainActivity extends AppCompatActivity {

  2.     private List<TestBean> mDatas;

  3.     private RecyclerView mRv;

  4.     private DiffAdapter mAdapter;

  5.  

  6.     @Override

  7.     protected void onCreate(Bundle savedInstanceState) {

  8.         super.onCreate(savedInstanceState);

  9.         setContentView(R.layout.activity_main);

  10.         initData();

  11.         mRv = (RecyclerView) findViewById(R.id.rv);

  12.         mRv.setLayoutManager(new LinearLayoutManager(this));

  13.         mAdapter = new DiffAdapter(this, mDatas);

  14.         mRv.setAdapter(mAdapter);

  15.     }

  16.  

  17.     private void initData() {

  18.         mDatas = new ArrayList<>();

  19.         mDatas.add(new TestBean("张旭童1", "Android", R.drawable.pic1));

  20.         mDatas.add(new TestBean("张旭童2", "Java", R.drawable.pic2));

  21.         mDatas.add(new TestBean("张旭童3", "背锅", R.drawable.pic3));

  22.         mDatas.add(new TestBean("张旭童4", "手撕产品", R.drawable.pic4));

  23.         mDatas.add(new TestBean("张旭童5", "手撕测试", R.drawable.pic5));

  24.     }

  25.  

  26.     /**

  27.      * 模拟刷新操作

  28.      *

  29.      * @param view

  30.      */

  31.     public void onRefresh(View view) {

  32.         try {

  33.             List<TestBean> newDatas = new ArrayList<>();

  34.             for (TestBean bean : mDatas) {

  35.                 newDatas.add(bean.clone());//clone一遍旧数据 ,模拟刷新操作

  36.             }

  37.             newDatas.add(new TestBean("赵子龙", "帅", R.drawable.pic6));//模拟新增数据

  38.             newDatas.get(0).setDesc("Android+");

  39.             newDatas.get(0).setPic(R.drawable.pic7);//模拟修改数据

  40.             TestBean testBean = newDatas.get(1);//模拟数据位移

  41.             newDatas.remove(testBean);

  42.             newDatas.add(testBean);

  43.             //别忘了将新数据给Adapter

  44.             mDatas = newDatas;

  45.             mAdapter.setDatas(mDatas);

  46.             mAdapter.notifyDataSetChanged();//以前我们大多数情况下只能这样

  47.         } catch (CloneNotSupportedException e) {

  48.             e.printStackTrace();

  49.         }

  50.     }

  51.  

  52. }

很简单,只不过在构建新数据源newDatas时,是遍历老数据源mDatas,调用每个data的clone()方法,确保新老数据源虽然数据一致,但是内存地址(指针不一致),这样在后面修改newDatas里的值时,不会牵连mDatas里的值被一起改了。

4 activity_main.xml 删掉了一些宽高代码,就是一个RecyclerView和一个Button用于模拟刷新。:

 
           
  1. <?xml version="1.0" encoding="utf-8"?>

  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

  3. >

  4.  

  5.     <android.support.v7.widget.RecyclerView

  6.         android:id="@+id/rv" />

  7.  

  8.     <Button

  9.         android:id="@+id/btnRefresh"

  10.         android:layout_alignParentRight="true"

  11.         android:onClick="onRefresh"

  12.         android:text="模拟刷新" />

  13. </RelativeLayout>

以上是一个普通青年很容易写出的,无脑notifyDataSetChanged()的demo,运行效果如第一节图一。 
但是我们都要争做文艺青年,so

下面开始进入正题,简单使用DiffUtil,我们需要且仅需要额外编写一个类。

想成为文艺青年,我们需要实现一个继承自DiffUtil.Callback的类,实现它的四个abstract方法。 
虽然这个类叫Callback,但是把它理解成:定义了一些用来比较新老Item是否相等的契约(Contract)、规则(Rule)的类, 更合适。

DiffUtil.Callback抽象类如下:

 
           
  1. public abstract static class Callback {

  2.    public abstract int getOldListSize();//老数据集size

  3.     public abstract int getNewListSize();//新数据集size

  4.     public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);//新老数据集在同一个postion的Item是否是一个对象?(可能内容不同,如果这里返回true,会调用下面的方法)

  5.      public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);//这个方法仅仅是上面方法返回ture才会调用,我的理解是只有notifyItemRangeChanged()才会调用,判断item的内容是否有变化

  6.  

  7.         //该方法在DiffUtil高级用法中用到 ,暂且不提

  8.         @Nullable

  9.      public Object getChangePayload(int oldItemPosition, int newItemPosition) {

  10.           return null;

  11.       }

  12.     }

本Demo如下实现DiffUtil.Callback,核心方法配有中英双语注释(说人话就是,翻译了官方的英文注释,方便大家更好理解)。

 
           
  1. /**

  2.  * 介绍:核心类 用来判断 新旧Item是否相等

  3.  */

  4.  

  5. public class DiffCallBack extends DiffUtil.Callback {

  6.     private List<TestBean> mOldDatas, mNewDatas;//看名字

  7.  

  8.     public DiffCallBack(List<TestBean> mOldDatas, List<TestBean> mNewDatas) {

  9.         this.mOldDatas = mOldDatas;

  10.         this.mNewDatas = mNewDatas;

  11.     }

  12.  

  13.     //老数据集size

  14.     @Override

  15.     public int getOldListSize() {

  16.         return mOldDatas != null ? mOldDatas.size() : 0;

  17.     }

  18.  

  19.     //新数据集size

  20.     @Override

  21.     public int getNewListSize() {

  22.         return mNewDatas != null ? mNewDatas.size() : 0;

  23.     }

  24.  

  25.     /**

  26.      * 被DiffUtil调用,用来判断 两个对象是否是相同的Item。

  27.      * 例如,如果你的Item有唯一的id字段,这个方法就 判断id是否相等。

  28.      * 本例判断name字段是否一致

  29.      */

  30.     @Override

  31.     public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {

  32.         return mOldDatas.get(oldItemPosition).getName().equals(mNewDatas.get(newItemPosition).getName());

  33.     }

  34.  

  35.     /**

  36.      * Called by the DiffUtil when it wants to check whether two items have the same data.

  37.      * 被DiffUtil调用,用来检查 两个item是否含有相同的数据

  38.      * DiffUtil uses this information to detect if the contents of an item has changed.

  39.      * DiffUtil用返回的信息(true false)来检测当前item的内容是否发生了变化

  40.      * DiffUtil uses this method to check equality instead of {@link Object#equals(Object)}

  41.      * DiffUtil 用这个方法替代equals方法去检查是否相等。

  42.      * so that you can change its behavior depending on your UI.

  43.      * 所以你可以根据你的UI去改变它的返回值

  44.      * 例如,如果你用RecyclerView.Adapter 配合DiffUtil使用,你需要返回Item的视觉表现是否相同。
        
    * 这个方法仅仅在areItemsTheSame()返回true时,才调用。

  45.      */

  46.     @Override

  47.     public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {

  48.         TestBean beanOld = mOldDatas.get(oldItemPosition);

  49.         TestBean beanNew = mNewDatas.get(newItemPosition);

  50.         if (!beanOld.getDesc().equals(beanNew.getDesc())) {

  51.             return false;//如果有内容不同,就返回false

  52.         }

  53.         if (beanOld.getPic() != beanNew.getPic()) {

  54.             return false;//如果有内容不同,就返回false

  55.         }

  56.         return true; //默认两个data内容是相同的

  57.     }

注释张写了这么详细的注释+简单的代码,相信一眼可懂。 
然后在使用时,注释掉你以前写的notifyDatasetChanged()方法吧,替换成以下代码:

 
           
  1. //文艺青年新宠

  2. //利用DiffUtil.calculateDiff()方法,传入一个规则DiffUtil.Callback对象,和是否检测移动item的 boolean变量,得到DiffUtil.DiffResult 的对象

  3.  

  4. DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true);

  5.  

  6. //利用DiffUtil.DiffResult对象的dispatchUpdatesTo()方法,传入RecyclerView的Adapter,轻松成为文艺青年

  7. diffResult.dispatchUpdatesTo(mAdapter);

  8.  

  9. //别忘了将新数据给Adapter

  10. mDatas = newDatas;

  11. mAdapter.setDatas(mDatas);

讲解:

步骤一

在将newDatas 设置给Adapter之前,先调用DiffUtil.calculateDiff()方法,计算出新老数据集转化的最小更新集,就是DiffUtil.DiffResult对象。 
DiffUtil.calculateDiff()方法定义如下: 
第一个参数是DiffUtil.Callback对象, 
第二个参数代表是否检测Item的移动,改为false算法效率更高,按需设置,我们这里是true。

 
           
  1. public static DiffResult calculateDiff(Callback cb, boolean detectMoves)

步骤二

然后利用DiffUtil.DiffResult对象的dispatchUpdatesTo()方法,传入RecyclerView的Adapter,替代普通青年才用的mAdapter.notifyDataSetChanged()方法。 
查看源码可知,该方法内部,就是根据情况调用了adapter的四大定向刷新方法。

 
           
  1. public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {

  2.   dispatchUpdatesTo(new ListUpdateCallback() {

  3.      @Override public void onInserted(int position, int count) {

  4.          adapter.notifyItemRangeInserted(position, count);

  5.     }

  6.  

  7.      @Override public void onRemoved(int position, int count) {

  8.          adapter.notifyItemRangeRemoved(position, count);

  9.      }

  10.  

  11.      @Override public void onMoved(int fromPosition, int toPosition) {

  12.          adapter.notifyItemMoved(fromPosition, toPosition);

  13.        }

  14.  

  15.       @Override public void onChanged(int position, int count, Object payload) {

  16.            adapter.notifyItemRangeChanged(position, count, payload);

  17.          }

  18.       });

  19. }


 
APP架构师 更多文章 【干货】Android开发新技术—Data Binding入门与实战 滴滴国际化项目 Android 端演进 【推荐】锤子系统开源项目一步 (One Step) 手机天猫解耦之路 dagger2 让你爱不释手:基础依赖注入框架篇
猜您喜欢 intel: CAT技术助力数据中心资源隔离 代码的深渊:2022年,一个试图用AI取代程序员的故事(1/4) 微服务架构设计(一):核心概念&amp;从既有的架构迁移到微服务的策略 揭秘 0.1 + 0.2 != 0.3