微信号:appjiagou

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

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

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

小结:

所以说,DiffUtil不仅仅只能和RecyclerView配合,我们也可以自己实现ListUpdateCallback接口的四个方法去做一些事情。(我暂时不负责任随便一项想,想到可以配合自己项目里的九宫格控件?或者优化我上篇文章写的NestFullListView?小安利,见 ListView、RecyclerView、ScrollView里嵌套ListView 相对优雅的解决方案:http://blog.csdn.net/zxt0601/article/details/52494665


至此,我们已进化成文艺青年,运行效果和第一节图二基本一致, 
唯一不同的是此时adapter.notifyItemRangeChanged()会有Item白光一闪的更新动画 (本文Demo的postion为0的item)。 这个Item一闪的动画有人喜欢有人恨,不过都不重要了, 
因为当我们学会了第三节的DiffUtil搞基用法,你爱不爱这个ItemChange动画,它都将随风而去。(不知道是不是官方bug) 
效果就是第一节的图二,我们的item0其实图片和文字都变化了,但是这个改变并没有伴随任何动画。 
让我们迈向 文艺青年中的文艺青年 之路。


三 DiffUtil的高级用法

理论:

高级用法只涉及到两个方法, 
我们需要分别实现DiffUtil.Callback的 
public Object getChangePayload(int oldItemPosition, int newItemPosition)方法, 
返回的Object就是表示Item改变了哪些内容。

再配合RecyclerView.Adapter的 
public void onBindViewHolder(VH holder, int position, List<Object> payloads)方法, 
完成定向刷新。(成为文青中的文青,文青青。) 
敲黑板,这是一个新方法,注意它有三个参数,前两个我们熟,第三个参数就包含了我们在getChangePayload()返回的Object。

好吧,那我们就先看看这个方法是何方神圣: 
在v7-24.2.0的源码里,它长这个样子:

 
           
  1.         /**

  2.          * Called by RecyclerView to display the data at the specified position. This method

  3.          * should update the contents of the {@link ViewHolder#itemView} to reflect the item at

  4.          * the given position.

  5.          * <p>

  6.          * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method

  7.          * again if the position of the item changes in the data set unless the item itself is

  8.          * invalidated or the new position cannot be determined. For this reason, you should only

  9.          * use the <code>position</code> parameter while acquiring the related data item inside

  10.          * this method and should not keep a copy of it. If you need the position of an item later

  11.          * on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will

  12.          * have the updated adapter position.

  13.          * <p>

  14.          * Partial bind vs full bind:

  15.          * <p>

  16.          * The payloads parameter is a merge list from {@link #notifyItemChanged(int, Object)} or

  17.          * {@link #notifyItemRangeChanged(int, int, Object)}.  If the payloads list is not empty,

  18.          * the ViewHolder is currently bound to old data and Adapter may run an efficient partial

  19.          * update using the payload info.  If the payload is empty,  Adapter must run a full bind.

  20.          * Adapter should not assume that the payload passed in notify methods will be received by

  21.          * onBindViewHolder().  For example when the view is not attached to the screen, the

  22.          * payload in notifyItemChange() will be simply dropped.

  23.          *

  24.          * @param holder The ViewHolder which should be updated to represent the contents of the

  25.          *               item at the given position in the data set.

  26.          * @param position The position of the item within the adapter's data set.

  27.          * @param payloads A non-null list of merged payloads. Can be empty list if requires full

  28.          *                 update.

  29.          */

  30.         public void onBindViewHolder(VH holder, int position, List<Object> payloads) {

  31.             onBindViewHolder(holder, position);

  32.         }

原来它内部就仅仅调用了两个参数的onBindViewHolder(holder, position) ,(题外话,哎哟喂,我的NestFullListView 的Adapter也有几分神似这种写法,看来我离Google大神又近了一步) 
看到这我才明白,其实onBind的入口,就是这个方法,它才是和onCreateViewHolder对应的方法, 
源码往下翻几行可以看到有个public final void bindViewHolder(VH holder, int position),它内部调用了三参的onBindViewHolder。 
关于RecyclerView.Adapter 也不是三言两句说的清楚的。(其实我只掌握到这里) 
好了不再跑题,回到我们的三参数的onBindViewHolder(VH holder, int position, List<Object> payloads),这个方法头部有一大堆英文注释,我一直觉得阅读这些英文注释对理解方法很有用处,于是我翻译了一下,

翻译:

由RecyclerView调用 用来在在指定的位置显示数据。 
这个方法应该更新ViewHolder里的ItemView的内容,以反映在给定的位置 Item(的变化)。 
请注意,不像ListView,如果给定位置的item的数据集变化了,RecyclerView不会再次调用这个方法,除非item本身失效了(invalidated ) 或者新的位置不能确定。 
出于这个原因,在这个方法里,你应该只使用 postion参数 去获取相关的数据item,而且不应该去保持 这个数据item的副本。 
如果你稍后需要这个item的position,例如设置clickListener。应该使用 ViewHolder.getAdapterPosition(),它能提供 更新后的位置。 
(二笔的我看到这里发现 这是在讲解两参的onbindViewHolder方法 
下面是这个三参方法的独特部分:) 
**部分(partial)绑定**vs完整(full)绑定 
payloads 参数 是一个从(notifyItemChanged(int, Object)或notifyItemRangeChanged(int, int, Object))里得到的合并list。 
如果payloads list 不为空,那么当前绑定了旧数据的ViewHolder 和Adapter, 可以使用 payload的数据进行一次 高效的部分更新。 
如果payload 是空的,Adapter必须进行一次完整绑定(调用两参方法)。
 
Adapter不应该假定(想当然的认为) 在那些notifyxxxx通知方法传递过来的payload, 一定会在 onBindViewHolder()方法里收到。(这一句翻译不好 QAQ 看举例就好) 
举例来说,当View没有attached 在屏幕上时,这个来自notifyItemChange()的payload 就简单的丢掉好了。 
payloads对象不会为null,但是它可能是空(empty),这时候需要完整绑定(所以我们在方法里只要判断isEmpty就好,不用重复判空)。 
作者语:这方法是一个高效的方法。 我是个低效的翻译者,我看了40+分钟。才终于明白,重要的部分已经加粗显示。


实战:

说了这么多话,其实用起来超级简单: 
先看如何使用getChangePayload()方法,又附带了中英双语注释

 
           
  1.     /**

  2.      * When {@link #areItemsTheSame(int, int)} returns {@code true} for two items and

  3.      * {@link #areContentsTheSame(int, int)} returns false for them, DiffUtil

  4.      * calls this method to get a payload about the change.

  5.      * 

  6.      * 当{@link #areItemsTheSame(int, int)} 返回true,且{@link #areContentsTheSame(int, int)} 返回false时,DiffUtils会回调此方法,

  7.      * 去得到这个Item(有哪些)改变的payload。

  8.      * 

  9.      * For example, if you are using DiffUtil with {@link RecyclerView}, you can return the

  10.      * particular field that changed in the item and your

  11.      * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} can use that

  12.      * information to run the correct animation.

  13.      * 

  14.      * 例如,如果你用RecyclerView配合DiffUtils,你可以返回  这个Item改变的那些字段,

  15.      * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} 可以用那些信息去执行正确的动画

  16.      * 

  17.      * Default implementation returns {@code null}.\

  18.      * 默认的实现是返回null

  19.      *

  20.      * @param oldItemPosition The position of the item in the old list

  21.      * @param newItemPosition The position of the item in the new list

  22.      * @return A payload object that represents the change between the two items.

  23.      * 返回 一个 代表着新老item的改变内容的 payload对象,

  24.      */

  25.     @Nullable

  26.     @Override

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

  28.         //实现这个方法 就能成为文艺青年中的文艺青年

  29.         // 定向刷新中的部分更新

  30.         // 效率最高

  31.         //只是没有了ItemChange的白光一闪动画,(反正我也觉得不太重要)

  32.         TestBean oldBean = mOldDatas.get(oldItemPosition);

  33.         TestBean newBean = mNewDatas.get(newItemPosition);

  34.  

  35.         //这里就不用比较核心字段了,一定相等

  36.         Bundle payload = new Bundle();

  37.         if (!oldBean.getDesc().equals(newBean.getDesc())) {

  38.             payload.putString("KEY_DESC", newBean.getDesc());

  39.         }

  40.         if (oldBean.getPic() != newBean.getPic()) {

  41.             payload.putInt("KEY_PIC", newBean.getPic());

  42.         }

  43.  

  44.         if (payload.size() == 0)//如果没有变化 就传空

  45.             return null;

  46.         return payload;//

  47.     }

简单的说,这个方法返回一个Object类型的payload,它包含了某个item的变化了的那些内容。 
我们这里使用Bundle保存这些变化。

在Adapter里如下重写三参的onBindViewHolder:

 
           
  1.     @Override

  2.     public void onBindViewHolder(DiffVH holder, int position, List<Object> payloads) {

  3.         if (payloads.isEmpty()) {

  4.             onBindViewHolder(holder, position);

  5.         } else {

  6.             //文艺青年中的文青

  7.             Bundle payload = (Bundle) payloads.get(0);

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

  9.             for (String key : payload.keySet()) {

  10.                 switch (key) {

  11.                     case "KEY_DESC":

  12.                         //这里可以用payload里的数据,不过data也是新的 也可以用

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

  14.                         break;

  15.                     case "KEY_PIC":

  16.                         holder.iv.setImageResource(payload.getInt(key));

  17.                         break;

  18.                     default:

  19.                         break;

  20.                 }

  21.             }

  22.         }

  23.     }

这里传递过来的payloads是一个List,由注释可知,一定不为null,所以我们判断是否是empty, 
如果是empty,就调用两参的函数,进行一次Full Bind。 
如果不是empty,就进行partial bind, 
通过下标0取出我们在getChangePayload方法里返回的payload,然后遍历payload的key,根据key检索,如果payload里携带有相应的改变,就取出来 然后更新在ItemView上。 
(这里,通过mDatas获得的也是最新数据源的数据,所以用payload的数据或者新数据的数据 进行更新都可以) 
至此,我们已经掌握了刷新RecyclerView,文艺青年中最文艺的那种写法。


四 在子线程中使用DiffUtil

在DiffUtil的源码头部注释中介绍了DiffUtil的相关信息, 
DiffUtil内部采用的Eugene W. Myers’s difference 算法,但该算法不能检测移动的item,所以Google在其基础上改进支持检测移动项目,但是检测移动项目,会更耗性能。 
在有1000项数据,200处改动时,这个算法的耗时: 
打开了移动检测时:平均值:27.07ms,中位数:26.92ms。 
关闭了移动检测时:平均值:13.54ms,中位数:13.36ms。 
有兴趣可以自行去源码头部阅读注释,对我们比较有用的是其中一段提到, 
如果我们的list过大,这个计算出DiffResult的时间还是蛮久的,所以我们应该将获取DiffResult的过程放到子线程中,并在主线程中更新RecyclerView。 
这里我采用Handler配合DiffUtil使用: 
代码如下:

 
           
  1.    private static final int H_CODE_UPDATE = 1;

  2.     private List<TestBean> mNewDatas;//增加一个变量暂存newList

  3.     private Handler mHandler = new Handler() {

  4.         @Override

  5.         public void handleMessage(Message msg) {

  6.             switch (msg.what) {

  7.                 case H_CODE_UPDATE:

  8.                     //取出Result

  9.                     DiffUtil.DiffResult diffResult = (DiffUtil.DiffResult) msg.obj;

  10.                     diffResult.dispatchUpdatesTo(mAdapter);

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

  12.                     mDatas = mNewDatas;

  13.                     mAdapter.setDatas(mDatas);

  14.                     break;

  15.             }

  16.         }

  17.     };

 
           
  1.             new Thread(new Runnable() {

  2.                 @Override

  3.                 public void run() {

  4.                     //放在子线程中计算DiffResult

  5.                     DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, mNewDatas), true);

  6.                     Message message = mHandler.obtainMessage(H_CODE_UPDATE);

  7.                     message.obj = diffResult;//obj存放DiffResult

  8.                     message.sendToTarget();

  9.                 }

  10.             }).start();

就是简单的Handler使用,不再赘述。


五总结和其他

1 其实本文代码量很少,可下载Demo查看,一共就四个类。 
但是不知不觉又被我写的这么长,主要涉及到了一些源码的注释的翻译,方便大家更好的理解。

2 DiffUtil很适合下拉刷新这种场景, 
更新的效率提高了,而且带动画,而且~还不用你动脑子算了。 
不过若是就做个删除 点赞这种,完全不用DiffUtils。自己记好postion,判断一下postion在不在屏幕里,调用那几个定向刷新的方法即可。

3 其实DiffUtil不是只能和RecyclerView.Adapter配合使用, 
我们可以自己实现 ListUpdateCallback接口,利用DIffUtil帮我们找到新旧数据集的最小差异集 来做更多的事情。

4 注意 写DEMO的时候,用于比较的新老数据集,不仅ArrayList不同,里面每个data也要不同。 否则changed 无法触发。 
实际项目中遇不到,因为新数据往往是网络来的。

5 今天是中秋节的最后一天,我们公司居然就开始上班了!!!气愤之余,我怒码一篇DiffUtil,我都不需要用DiffUtil,也能轻易比较出我们公司和其他公司的差异。QAQ,而且今天状态不佳,居然写了8个小时才完工。本以为这篇文章是可以入选微作文集的,没想到也是蛮长的。没有耐心的其实可以下载DEMO看看,代码量没多少,使用起来还是很轻松的。

6 关于“白光一闪”onChange动画, 
public Object getChangePayload() 这个方法返回不为null的话,onChange采用Partial bind,就不会出现。 反之就有。

github传送门:好用给个star呗 
https://github.com/mcxtzhang/DiffUtils

CSDN传送门: 
http://download.csdn.net/detail/zxt0601/9632159


 
APP架构师 更多文章 详解7.0带来的新工具类:DiffUtil(上) 【干货】Android开发新技术—Data Binding入门与实战 滴滴国际化项目 Android 端演进 【推荐】锤子系统开源项目一步 (One Step) 手机天猫解耦之路
猜您喜欢 (45) 神奇的堆 / 计算机程序的思维逻辑 有交互项的多元线性回归——OLS回归模型拟合(四) 开心网卖身:从偷菜大火到错失整个互联网 大数据和云计算将成为未来国家发展战略