移动设备屏幕大小有限(不得不说我是顽固的小屏爱好者,大于5.5寸难以接受,时代已经抛弃我了哈哈),列表(List)能够说是一个出现很是高频的交互设计。大多数状况下咱们的列表不只仅是一次性加载本地数据,而要应付来自网络的各类动态内容,多是增长、删除等操做。java
在Android开发中,一个耳熟能详的方法就是 notifyDataSetChanged
,在适配器(Adapter)的设计模式下,每当咱们的列表数据发生变动时,就须要调用此方法来更新UI。然而,这个方法并不“节能”,它会同时刷新列表中的全部item,包括那些并无变化的数据,这样就带来不少计算资源的浪费。要知道,从你的一个 setText
或者 setImageResource
方法调用到最终呈现到屏幕上,软件到硬件,中间经历了很是复杂的过程。基于能省则省的移动开发原则,有没有更好的办法呢?算法
谷歌确实也考虑到了这个问题,因此不知道在何时(暂时没有去查阅)推出了DiffUtil这个解决方案。在RecyclerView的依赖包下面,能够看到,除了DiffUtil,还有异步处理数据等一系列有趣的工具。 设计模式
这样作的好处就是避免了没必要要的UI更新,DiffUtil计算出差别以后,只刷新产生变更的item。具体地,咱们能够在Adapter的 onBindViewHolder
方法打断点或者日志观察,或者调用 registerAdapterDataObserver
方法监听item的各类操做状况。其次,之前的 notifyDataSetChanged
方法因为会刷新整个列表因此没有原生的动画效果,而DiffUtil内部最终调用了各类 notifyItemXXX
方法。数组
DiffUtil的使用也很简单:网络
一、先实现比较新旧数据的回调,能够是一个独立的类,也能够写成Adapter的内部类:异步
public class BaseXXXAdapter<T> extends RecyclerView.Adapter {
// ...
private class DiffCallback extends DiffUtil.Callback {
private List<T> oldData, newData;
DiffCallback(List<T> oldData, List<T> newData) {
this.oldData = oldData;
this.newData = newData;
}
@Override
public int getOldListSize() {
return oldData.size();
}
@Override
public int getNewListSize() {
return newData.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
T oldT = oldData.get(oldItemPosition);
T newT = newData.get(newItemPosition);
// 实际状况最好是在此处对比新旧数据的id(好比用户uid),这里为了方便示例直接equals对象了
// 若此处返回true,则DiffUtil会再调用下面的areContentsTheSame方法,进一步对比UI是否有变化
// 若此处返回false,则说明id都不一样,确定不是一个item
return Objects.equals(oldT, newT);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
// TODO 比较新旧数据(主要是UI展现内容)是否相同,这里为了方便示例直接返回true
return true;
}
}
}
复制代码
二、而后在Adapter内部实现一个update数据的方法:ide
@Override
public void updateData(List<T> newData) {
DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffCallback(getData(), newData));
// 这里的getData即表示获取整个列表的数据,自行实现便可
getData().clear();
getData().addAll(newData);
result.dispatchUpdatesTo(this);
}
复制代码
注意这里的 dispatchUpdatesTo
能够在clear以前,也能够在addAll以后,实际效果暂未发现什么区别,以前查阅资料包括官方示例也都是最后执行dispatch,姑且认为这样算标准吧。工具
三、……咦,怎么才两步,确实就这么简单。重点仍是 areItemsTheSame
和 areContentsTheSame
方法,后者大部分时候只须要对比每一个item上UI展现出来的数据便可,由于用户只关心眼见的内容。动画
咱们会发如今上面的使用示例中,updateData
方法内部对原数据进行了清除和添加的操做,这会致使一个问题即是:列表数据集合中的对象已经变了,即便其某项对应的UI内容没有发生变化。ui
举个例子,一个通信录列表里面有 [小明, 小红] 两我的,对应内存地址为 [a1, a2],如今经过上述 updateData
方法更新了通信录列表,UI内容变成了 [小王, 小红],对应内存地址为 [b1, b2]。对用户来讲小红这个item看上去没有发生变化,但其实对应的数据类对象已经不一样。并且此时 onBindViewHolder
方法只会触发一次,将小明更新成小王,而不会触发小红那个position对应的 onBindViewHolder
。
上述细节很关键,若是开发过程当中绑定(bind)数据不恰当的话,就容易形成各类奇异问题,好比网上资料最多的DiffUtil致使item点击事件数据错位问题、数组越界崩溃问题等等。
这里的“不恰当”,绝大部分状况下,总结出来:其实指的就是在 onBindViewHolder
方法中持有了某个位置(position)对应数据的不可变对象。最多见的误用示例就是在 onBindViewHolder
中设置某些控件的点击事件并引用数据对象:
// 此处假设item的数据类为User
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
MyItemViewHolder h = (MyItemViewHolder) holder;
User user = getData().get(position);
h.mNameTextView.setOnClickListener(v -> {
// 第2种写法:User user = getData().get(position);
// 假设这里是点击item跳转到该User对应的我的主页界面
startWebView(user.getHomePageUrl());
});
}
复制代码
在不接入DiffUtil以前,上面这段代码没有任何问题,由于咱们都是使用 notifyDataSetChanged
方法来更新UI,每次更新调用到 onBindViewHolder
时,点击事件都会从新设置,get出来的user对象天然也是最新的。一旦咱们使用了DiffUtil,就会出问题了。
回到上面小王绿了小明的例子,在咱们的 updateData
方法执行后,若是咱们只对比了user的名字这个属性(其实也只须要对比这个属性),那么小红那一个item就不会触发对应的 onBindViewHolder
,即小红的点击事件回调里,仍然持有着旧数据集的user对象(对应那个内存地址a2)。但实际上小红应该对应 b2 那个内存了,这就形成 a2 内存没法释放,问题是否是显得有点严重了。
有同窗说无所谓呀,反正点击事件依然有效。那若是我说网络数据刷新下来小红的 homePageUrl 变了呢?是否是还得把这个属性加入DiffUtil的对比方法中?这样最终会致使小红的 onBindViewHolder
方法也执行,跟 notifyDataSetChanged
岂不是没什么两样了?
此外,若get对象写成注释中的第2种写法,且列表第0个位置的item被删了呢?小红顶上去变成了第0个,此时因为小红的UI内容没变,只是位置变了,因此 onBindViewHolder
依然不会执行。以上面的示例代码来看,当再次点击小红时,就会直接出现数组越界的异常。由于position仍是以前的1,而此时小红的position已经为0。
显然上述出现的这些问题不符合谷歌的设计初衷,也不符合咱们使用DiffUtil的初衷。其实解决办法很简单,就是要对 onBindViewHolder
方法有一个正确的认知,其原则就是:
onBindViewHolder
只作UI内容的更新,如 setText
,setImageXXX
等方法。作到数据对象一次性使用。onBindViewHolder
中设置点击事件监听。正确的点击事件监听仍是参照以下形式比较好:
// 好比这是某个Base适配器类
public class BaseXXXAdapter<T> extends RecyclerView.Adapter {
// ...
private View.OnClickListener mOnClickListener;
private View.OnLongClickListener mOnLongClickListener;
private OnItemClickListener mOnItemClickListener;
public interface OnItemClickListener {
void onItemClick(View view, RecyclerView.ViewHolder holder, int position);
void onItemLongClick(View view, RecyclerView.ViewHolder holder, int position);
}
public BaseXXXAdapter(Context context) {
// ...
mOnClickListener = v -> {
RecyclerView.ViewHolder h = (RecyclerView.ViewHolder) v.getTag();
int pos = h.getAdapterPosition();
if (mOnItemClickListener != null) {
mOnItemClickListener.onItemClick(v, h, pos);
}
};
mOnLongClickListener = v -> {
RecyclerView.ViewHolder h = (RecyclerView.ViewHolder) v.getTag();
int pos = h.getAdapterPosition();
if (mOnItemClickListener != null) {
mOnItemClickListener.onItemLongClick(v, h, pos);
}
return true;
};
}
public void setOnItemClickListener(OnItemClickListener clickListener) {
this.mOnItemClickListener = clickListener;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// ...省略holder实例化
holder.itemView.setTag(holder); // 把holder当tag存
holder.itemView.setOnClickListener(mOnClickListener);
holder.itemView.setOnLongClickListener(mOnLongClickListener);
return holder;
}
}
// 继承实现的实际业务Adapter
public class XXXAdapter extends BaseXXXAdapter<User> {
public XXXAdapter(Context context) {
setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(View view, RecyclerView.ViewHolder holder, int position) {
MyItemViewHolder h = (MyItemViewHolder) holder;
// 每次点击都保证了为对应位置的数据,不再用担忧数据错位问题了
User user = getData().get(position);
}
@Override
public void onItemLongClick(View view, RecyclerView.ViewHolder holder, int position) {
// ...
}
});
}
}
复制代码