概述
在RecyclerView数据更新神器 - DiffUtil 译文中,已经了解到DiffUtil如何神助RecyclerView进行UI更新。它有一个缺陷就是DiffUtil在计算新旧数据集差异时需要开启线程,而在更新UI时又要在主线程。尽管可以这么做:
- Thread + Handler
- RxJava
虽然这样可以实现,但又显得笨拙。在support-v7:27.1.0又新增了一个DiffUtil的封装类 - AsyncListDiffer,它在后台线程使用DiffUtil计算旧数据集->新数据集的最小量,同时又在主线程中将更新操作的最小量分发给ListUpdateCallback,以完成数据更新,从而弥补了DiffUtil的缺陷。
核心类
DiffUtil.ItemCallback
与DIffUtil.Callback方法类似,DiffUtil在计算旧数据集->新数据集时,回调它。在DiffUtil.Callback中,提供两种处理角色 ->Item的列表索引和Item差异。而,在DiffUtil.ItemCallback中只处理Item差异。它允许从UI和内容特定的差异代码中分离索引到数组或者List中。
/**
* DiffUtil在计算两个列表之间的非空Item的差异时使用的回调类。
* DiffUtil.Callback提供两种处理角色 - 列表索引和item差异。而,DiffUtil.ItemCallback只处理第二个,它允许从UI和内容特定的差异代码中分离索引到数组或者List中。
*/
public abstract static class ItemCallback<T> {
/**
* 用来判断 两个对象是否是相同的Item
* 例如,如果你的Item有唯一的id字段,这个方法就 判断id是否相等。
*/
public abstract boolean areItemsTheSame(T oldItem, T newItem);
/**
* 当它想要检查两个Item是否具有相同的数据时由DiffUtil调用。
* DiffUtil使用返回的信息来判断内容是否已更改
* DiffUtil 用这个方法替代equals方法去检查是否相等,以便根据UI更改其行为
* 例如,如果你用RecyclerView.Adapter配合DiffUtil使用,需要返回Item的视觉表现是否相同。
* 这个方法仅仅在areItemsTheSame()返回true时,才调用。
*
* 返回true,表示新旧数据集中,当前位置的item内容相同,否则,不同
*/
public abstract boolean areContentsTheSame(T oldItem, T newItem);
/**
* 当areItemsTheSame(int, int)返回true并且areContentsTheSame(int, int)返回false时,DiffUtil才会调用此方法,以获取该item改变的payload
* 例如,如果你用RecyclerView配合DiffUtils,你可以返回 这个Item改变的那些字段,RecyclerView.ItemAnimator可以使用哪些信息执行动画
* 默认的实现是返回null
* 返回 一个 代表着新老item的改变内容的 payload对象
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public Object getChangePayload(T oldItem, T newItem) {
return null;
}
}
AsyncDifferConfig
AsyncDifferConfig是ListAdapter,AsyncListDiffer和后台线程的配置对象。在其内,至少定义一个DiffUtil.ItemCallback,用于DiffUtil在计算旧数据集->新数据集时回调。
AsyncDifferConfig.Builder
AsyncDifferConfig.Builder是AsyncDifferConfig构建类,主要是用来配置Diff计算数据集差异时的后台线程和DiffUtil.ItemCallback回调。
- AsyncDifferConfig.Builder (DiffUtil.ItemCallback diffCallback)
在创建AsyncDifferConfig.Builder是必须传递一个DiffUtil.ItemCallback对象,用于DiffUtil计算数据集差异时回调。
当AsyncDifferConfig.Builde调用build()
创建AsyncDifferConfig对象时,如果未设置指定的后台线程,那么将自动创建一个主线程Executor和长度为2的线程池。也就是说AsyncDifferConfig持有一个长度为2的线程池,由所有的AsyncListDiffer对象共用。如果想指定DiffUtil计算的后台线程,可调用setBackgroundThreadExecutor(Executor executor)
方法
核心方法
- getCurrentList(): 获取当前数据集
- submitList(List newList):更新新数据集
核心代码解析
每次在更新数据集时,都需要调用AsyncListDiffer的submitList(List<T> newList)
方法,下面来看下,这个方法中做了哪些事:
1 public void submitList(final List<T> newList) {
2 if (newList == mList) {
3 // nothing to do
4 return;
5 }
6
7 // 用于临时
8 final int runGeneration = ++mMaxScheduledGeneration;
9
10 if (newList == null) {
11 //noinspection ConstantConditions
12 mUpdateCallback.onRemoved(0, mList.size());
13 mList = null;
14 mReadOnlyList = Collections.emptyList();
15 return;
16 }
17
18 if (mList == null) {
19 // fast simple first insert
20 mUpdateCallback.onInserted(0, newList.size());
21 mList = newList;
22 mReadOnlyList = Collections.unmodifiableList(newList);
23 return;
24 }
25
26 final List<T> oldList = mList;
27 mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
28 @Override
29 public void run() {
30 final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
31 @Override
32 public int getOldListSize() {
33 return oldList.size();
34 }
35
36 @Override
37 public int getNewListSize() {
38 return newList.size();
39 }
40
41 @Override
42 public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
43 return mConfig.getDiffCallback().areItemsTheSame(
44 oldList.get(oldItemPosition), newList.get(newItemPosition));
45 }
46
47 @Override
48 public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
49 return mConfig.getDiffCallback().areContentsTheSame(
50 oldList.get(oldItemPosition), newList.get(newItemPosition));
51 }
52 });
53
54 mConfig.getMainThreadExecutor().execute(new Runnable() {
55 @Override
56 public void run() {
57 if (mMaxScheduledGeneration == runGeneration) {
58 latchList(newList, result);
59 }
60 }
61 });
62 }
63 });
64 }
65 private void latchList(@NonNull List<T> newList, @NonNull DiffUtil.DiffResult diffResult) {
66 diffResult.dispatchUpdatesTo(mUpdateCallback);
67 mList = newList;
68 mReadOnlyList = Collections.unmodifiableList(newList);
69 }
在2-5行中,首先判断newList与AsyncListDiffer中缓存的数据集mList是否为同一个对象,如果是的话,直接返回。这也就是说,调用submitList()
方法所传递的数据集时,需要new一个新的List。
在8行,定义了变量runGeneration,用于缓存当前预执行线程的次数的最大值。至于作用后续再说。
在10-16行中,判断newList是否为null。若newList为null,将移除所有Item的操作分发给ListUpdateCallback,mList置为null,同时将只读List - mReadOnlyList清空,最后返回。
在18-24行中,判断newList是否为null。若mList为null,表示这是第一次向Adapter添加数据集,此时将添加最新数据集操作分发给ListUpdateCallback,将mList设置为newList,同时以newList为原本重建创建只读List副本 - mReadOnlyList,最后返回。
在27-63中,AsyncDifferConfig对象获取后台线程,并运行该后台线程。在该线程中,创建一个新的DiffUtil.Callback()回调同时DiffUtil在计算旧数据集->新数据集的最小量时回调该Callbcak。而,这个DiffUtil.Callback在处理Item差异时,调用的是所传递过去的DiffUtil.ItemCallback实例中的areItemsTheSame()
方法和areContentsTheSame()
。这里需要注意的一点是:在这个DiffUtil.Callback中,并没有重写getChangePayload()
方法,这不仅意味着它并没有实现高效地局部绑定,同时自定义DiffUtil.ItemCallback时也没有必要实现getChangePayload()
方法。
当DiffUtil计算新旧数据集差异之后,首先判断runGeneration与当前预执行线程的次数的最大值是否相等,如果相等,在主线程中将更新操作分发给ListUpdateCallback,将mList设置为newList,同时以newList为原本重建创建只读List副本 - mReadOnlyList,最后返回。如果不相等,表示在此线程运行以后,再一次调用了submitList()
方法用于传递新的数据集,此时将不做任何处理,待DiffUtil计算结束以后,再做相应处理。
值得注意的两点就是:
- 调用
submitList()
方法所传递的数据集时,需要new一个新的List。 - 在这个DiffUtil.Callback中,并没有重写getChangePayload()方法,这不仅意味着它并没有实现高效地局部绑定,同时自定义DiffUtil.ItemCallback时也没有必要实现getChangePayload()方法。
既然已经了解了submitList()
方法的实现方式,如果对没有实现高效地局部绑定,完全可以自定义AsyncListDiffer,然后在创建DiffUtil.Callback()回调时,重写getChangePayload()
方法即可。当然在Apdaer中也需要重写onBindViewHolder(holder: DiffViewHolder, position: Int, payloads: MutableList<Any>)
方法。
简单应用
与DiffUtil相比较,AsyncListDiffer使用更加的方便,只需要这么做:
创建Bean
data class DiffBean(var name: String, var desc: String) { override fun equals(o: Any?): Boolean { if (this === o) return true if (o == null || javaClass != o.javaClass) return false val diff = o as DiffBean? return diff!!.name == name } override fun hashCode(): Int { var result = name?.hashCode() ?: 0 return result } }
创建DiffUtil.ItemCallback的实现类
class DiffItemCallback : DiffUtil.ItemCallback<DiffBean>() { override fun areItemsTheSame(oldItem: DiffBean?, newItem: DiffBean?): Boolean { return oldItem?.name == newItem?.name } override fun areContentsTheSame(oldItem: DiffBean?, newItem: DiffBean?): Boolean { if (oldItem?.desc != newItem?.desc) { return false } // //默认两个data内容是相同的 return true } }
自定义RecyclerView.Adapter。
class AsyncListDifferAdapter : RecyclerView.Adapter<DiffViewHolder>() { val mAld = AsyncListDiffer<DiffBean>(this, DiffItemCallback()) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DiffViewHolder { val view: View = LayoutInflater.from(parent.context) .inflate(R.layout.item_diff, parent, false) return DiffViewHolder(view) } override fun getItemCount(): Int { return mAld.currentList.size } override fun onBindViewHolder(holder: DiffViewHolder, position: Int) { val diffBean: DiffBean = mAld.currentList[position] holder.tvName.text = diffBean.name holder.tvDesc.text = diffBean.desc } fun submitList(list: List<DiffBean>?) { mAld.submitList(list) } }
需要注意的是:
- 在Adapter中,创建一个AsyncListDiffer实例
- 定义
submitList(list: List<DiffBean>?)
方法,在这个方法中AsyncListDiffer实例调用submitList()
方法用来更新数据集。 - 如果想获取数据集,只需AsyncListDiffer实例调用
getCurrentList()
方法即可
更新数据集
val newList = mutableListOf<DiffBean>() newList.addAll(mList) mAdapter.submitList(newList)
ListAdapter
ListAdapter是基于RecyclerView.Adapter的AsyncListDiffer封装类,用于在RecyclerView中呈现列表数据。在其内创建了AsyncListDiffer的示例,以便在后台线程中使用DiffUtil计算新旧数据集的差异。
核心方法
为了便于操作Item数据,
- getItemCount():获取数据集的Size
- submitList(List list):传递新的数据集
- getItem(int position):获取Item
简单使用
创建DiffUtil.ItemCallback的实现类
class DiffItemCallback : DiffUtil.ItemCallback<DiffBean>() { override fun areItemsTheSame(oldItem: DiffBean?, newItem: DiffBean?): Boolean { return oldItem?.name == newItem?.name } override fun areContentsTheSame(oldItem: DiffBean?, newItem: DiffBean?): Boolean { if (oldItem?.desc != newItem?.desc) { return false } // //默认两个data内容是相同的 return true } }
创建ListApapter实现类
class DiffListAdapter : ListAdapter<DiffBean, DiffViewHolder> { constructor() : this(DiffItemCallback()) constructor(itemCallback: DiffItemCallback) : super(itemCallback) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DiffViewHolder { val view: View = LayoutInflater.from(parent.context) .inflate(R.layout.item_diff, parent, false) return DiffViewHolder(view) } override fun onBindViewHolder(holder: DiffViewHolder, position: Int) { val diffBean: DiffBean = getItem(position) holder.tvName.text = diffBean.name holder.tvDesc.text = diffBean.desc } }
更新数据集
val newList = mutableListOf<DiffBean>() newList.addAll(mList) mAdapter.submitList(newList)
在更新数据集时,只需调用ListAdapter的submitList(List<T> list)
方法即可,就像在AsyncListDiffer示例一样,在这个方法中也是调用了 AsyncListDiffer的submitList(List<T> list)
方法。
如果想获取当前数据集的长度,可以调用getItemCount()
方法。
如果想获取某个Item中的对象,可以调用getItem(int position)
方法。
总结
AsyncListDiffer解决了DiffUtil在计算新旧数据集差异时需要开启线程,而在更新UI时又要在主线程的缺陷,它更像DiffUtil的工具类,配合RecylcerView.Adapter使用。同时,开阔了对于DiffUtil处理的方式,我们可以照着这个思路,去创建适合自己的AsyncListDiffer。