码迷,mamicode.com
首页 > 移动开发 > 详细

【Android】新的工具类DiffUtil,让RecyclerView上天

时间:2021-01-01 12:28:49      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:lse   更新   null   用法   通知   cycle   这一   http   sha   

本文由张旭童投稿。

张旭童的博客地址:

http://blog.csdn.net/zxt0601/

跟作者的几次沟通后发现作者非常的贴心,对了,该类不是7.0才能用,其兼容到SDK 7,属于support-v7中的类。还有该类的思想,有兴趣可以应用到ListView中,如果你非常喜欢ListView的话。

不知道大家今天要不要上班,反正我是要上班了~祝大家和自己工作愉快

恩,我的御用作图妹子竟然还在休假,还是没带电脑那种,所以今天的封面图就凑合了~~

1 概述

DiffUtil是support-v7:24.2.0中的新工具类,它用来比较两个数据集,寻找出旧数据集-》新数据集的最小变化量。

说到数据集,相信大家知道它是和谁相关的了,就是我的最爱,RecyclerView。

就我使用的这几天来看,它最大的用处就是在RecyclerView刷新时,不再无脑mAdapter.notifyDataSetChanged()。

以前无脑mAdapter.notifyDataSetChanged()有两个缺点:

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

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

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


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

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


adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
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部分类、方法 官方注释的汉化

2 DiffUtil的简单用法

前文也提到,DiffUtil是帮助我们在刷新RecyclerView时,计算新老数据集的差异,并自动调用RecyclerView.Adapter的刷新方法,以完成高效刷新并伴有Item动画的效果。

这里需要读者脑补一个简单的RecyclerView展示列表的代码,当然还有个按钮点击模拟刷新。

ok,脑补成功,继续...

下面开始进入正题,简单使用DiffUtil,我们需要且仅需要额外编写一个类。
想成为文艺青年,我们需要实现一个继承自DiffUtil.Callback的类,实现它的四个abstract方法。

虽然这个类叫Callback,但是把它理解成:定义了一些用来比较新老Item是否相等的契约(Contract)、规则(Rule)的类, 更合适。

DiffUtil.Callback抽象类如下:
技术图片

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

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

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

public static DiffResult calculateDiff(Callback cb, boolean detectMoves)```


**步骤二**

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

![](https://s4.51cto.com/images/blog/202012/27/fa268cfbc518b88afd7877c06b227e77.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
**小结:**

所以说,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其实图片和文字都变化了,但是这个改变并没有伴随任何动画。 

让我们迈向 文艺青年中的文艺青年 之路。

**3   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的源码里,它长这个样子:

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



原来它内部就仅仅调用了两个参数的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()方法,又附带了中英双语注释
![](https://s4.51cto.com/images/blog/202012/27/41d4024ac7b28a62256298fcfdb21c1f.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
简单的说,这个方法返回一个Object类型的payload,它包含了某个item的变化了的那些内容。 
我们这里使用Bundle保存这些变化。

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

![](https://s4.51cto.com/images/blog/202012/27/1ebf79529f60fe4504bf9bcc373dc358.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
这里传递过来的payloads是一个List,由注释可知,一定不为null,所以我们判断是否是empty, 
如果是empty,就调用两参的函数,进行一次Full Bind。 
如果不是empty,就进行partial bind, 
通过下标0取出我们在getChangePayload方法里返回的payload,然后遍历payload的key,根据key检索,如果payload里携带有相应的改变,就取出来 然后更新在ItemView上。 
(这里,通过mDatas获得的也是最新数据源的数据,所以用payload的数据或者新数据的数据 进行更新都可以) 
至此,我们已经掌握了刷新RecyclerView,文艺青年中最文艺的那种写法。

**4   总结和其他**

* 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,就不会出现。 反之就有。

源码下载:
https://github.com/mcxtzhang/DiffUtils

掘金是一个高质量的技术社区,从 RxJava 到 React Native,性能优化到优秀开源库,让你不错过 Android 开发的每一个技术干货。长按图片二维码识别或者各大应用市场搜索「掘金」,技术干货尽在掌握中。

![](https://s4.51cto.com/images/blog/202012/27/17b357350754343684189c055b7c14a4.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

如果你有好的文章想和大家分享,欢迎投稿,直接向我投递文章链接即可。

欢迎长按下图->识别图中二维码或者扫一扫关注我的公众号:

![](https://s4.51cto.com/images/blog/202012/27/dd90265a28b9cf0b42af061e8a2cdea6.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

阅读原文

【Android】新的工具类DiffUtil,让RecyclerView上天

标签:lse   更新   null   用法   通知   cycle   这一   http   sha   

原文地址:https://blog.51cto.com/15064646/2575299

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!