RecyclerView相关的文章预计会写六篇,此处是第二篇
- RecyclerView中的position
- RecyclerView中的DiffUtil
- RecyclerView中的SnapHelper
- RecyclerView中的Selection
- RecyclerView中的ConcatAdapter
- RecyclerView中的Glide预加载
是什么?
DiffUtil是一个工具类,用来处理列表更新时的差异,计算出更新的部分,输出给RecyclerView来只刷新更新的部分,提高刷新效率。
平时我们开发过程中如果列表内容或列表项发生变化,我们会触发 notifyDataSetChanged更新列表,但 notifyDataSetChanged方法效率低下,它会告知RecyclerView整个列表已经无效,
导致整个列表重新绑定和重绘,包括列表中看不到的项,当列表数据过大时,可能会带来闪烁或卡顿问题,影响用户体验。
一种解决办法是使用notifyItemxxx方法,只更新RecyclerView中变化的内容,对于一些简单场景且已知Item位置的情况下可以使用此种方式,很多情况下我们无法确切直到要更新Item的位置,这时候就需要DiffUtil出场了。
在介绍DiffUtil之前,我们先介绍一下它的两个孪生兄弟 ListAdapter和 AsyncListDiffer。
此androidx.recyclerview.widget.ListAdapter非彼android.widget.ListAdapter。
ListAdapter,AsyncListDiffer和DiffUtil一样,可以实现Item差异比对,是对DiffUtil的封装简化,他们在子线程执行Diff操作,并在主线程更新操作结果。
AsyncListDiffer使用DiffUtil实现,ListAdapter使用AsyncListDiffer实现,一层层递进。
下面先介绍ListAdapter
ListAdapter使用
ListAdapter继承自RecyclerView.Adapter,封装了AsyncListDiffer,为RecyclerView提供列表数据。
class MyListAdapter : ListAdapter<AdapterType, MyViewHolder>(
DiffItemCallback()
) {
private lateinit var onClickListener: OnClickListener;
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.adapter_item,
parent,
false
)
)
}
fun setClickListener(listener: OnClickListener) {
onClickListener = listener
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
Log.d("MyListAdapter", "position: " + position)
holder.bind(getItem(position), onClickListener)
}
}
private class DiffItemCallback : DiffUtil.ItemCallback<AdapterType>() {
override fun areItemsTheSame(oldItem: AdapterType, newItem: AdapterType): Boolean {
Log.d("MyListAdapter", "oldItem: " + oldItem.id + ",newItem: " + newItem.id)
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: AdapterType, newItem: AdapterType): Boolean {
Log.d("MyListAdapter", "oldItem: " + oldItem.name + ",newItem: " + newItem.name)
return oldItem.name == newItem.name
}
}
var adapters = mutableListOf<AdapterType>(
AdapterType(0, "Adapter"),
AdapterType(1, "Adapter2"), AdapterType(2, "Adapter3")
)
val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
val adapter = MyListAdapter()
adapter.setClickListener(OnClickListener {
var newList = mutableListOf<AdapterType>()
newList.addAll(adapters)
newList[1] = AdapterType(1, "My Adapter")
adapter.submitList(newList)
Toast.makeText(this, "click", Toast.LENGTH_SHORT).show() })
adapter.submitList(adapters)
recyclerView.adapter = adapter
小结
- ListAdapter构造方法中传入了一个DiffUtil.ItemCallback,并重写了areItemsTheSame和areContentsTheSame来判断是否相同Item;
- 在获取Item时需要使用getItem(position),不能直接从数据中获取对应Item;
- 更新数据(增,删,改)时需要调用submitList并传入不同的List对象,才会触发Diff的逻辑,不能直接调用notifyDataSetChange,这个设计有点看不懂,不过看一下源码,可以很容易理解原因。
AsyncListDiffer使用
AsyncListDiffer封装了DiffUtil,可以从ListAdapter源码中看到AsyncListDiffer的用法。
private class MyDifferAdapter: RecyclerView.Adapter<MyViewHolder>() {
private val mDiffer = AsyncListDiffer<AdapterType>(this, DiffItemCallback())
private lateinit var onClickListener: OnClickListener;
fun setClickListener(listener: OnClickListener) {
onClickListener = listener
}
fun submitList(list: List<AdapterType>) {
mDiffer.submitList(list)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.adapter_item,
parent,
false
)
)
}
override fun getItemCount(): Int {
return mDiffer.currentList.size
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
Log.d("MyDifferAdapter", "position: " + position)
holder.bind(mDiffer.currentList.get(position), onClickListener)
}
}
val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
//val adapter = MyListAdapter()
val adapter = MyDifferAdapter()
adapter.setClickListener(OnClickListener {
var newList = mutableListOf<AdapterType>()
newList.addAll(adapters)
newList[1] = AdapterType(1, "My Adapter")
adapter.submitList(newList)
Toast.makeText(this, "click", Toast.LENGTH_SHORT).show() })
adapter.submitList(adapters)
recyclerView.adapter = adapter
小结
- 操作List的数据需要通过mDiffer.currentList完成,不能直接访问列表数据;
- 更新列表需要通过submitList,和ListAdapter类似;
DiffUtil使用
首先,我们先来认识一下DiffUtil.Callback和DiffUtil.DiffResult
/**
* A Callback class used by DiffUtil while calculating the diff between two lists.
*/
public abstract static class Callback {
/**
* Returns the size of the old list.
*
* @return The size of the old list.
*/
public abstract int getOldListSize();
/**
* Returns the size of the new list.
*
* @return The size of the new list.
*/
public abstract int getNewListSize();
/**
* Called by the DiffUtil to decide whether two object represent the same Item.
* <p>
* For example, if your items have unique ids, this method should check their id equality.
*
* @param oldItemPosition The position of the item in the old list
* @param newItemPosition The position of the item in the new list
* @return True if the two items represent the same object or false if they are different.
*/
public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);
/**
* Called by the DiffUtil when it wants to check whether two items have the same data.
* DiffUtil uses this information to detect if the contents of an item has changed.
* <p>
* DiffUtil uses this method to check equality instead of {@link Object#equals(Object)}
* so that you can change its behavior depending on your UI.
* For example, if you are using DiffUtil with a
* {@link RecyclerView.Adapter RecyclerView.Adapter}, you should
* return whether the items' visual representations are the same.
* <p>
* This method is called only if {@link #areItemsTheSame(int, int)} returns
* {@code true} for these items.
*
* @param oldItemPosition The position of the item in the old list
* @param newItemPosition The position of the item in the new list which replaces the
* oldItem
* @return True if the contents of the items are the same or false if they are different.
*/
public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
/**
* When {@link #areItemsTheSame(int, int)} returns {@code true} for two items and
* {@link #areContentsTheSame(int, int)} returns false for them, DiffUtil
* calls this method to get a payload about the change.
* <p>
* For example, if you are using DiffUtil with {@link RecyclerView}, you can return the
* particular field that changed in the item and your
* {@link RecyclerView.ItemAnimator ItemAnimator} can use that
* information to run the correct animation.
* <p>
* Default implementation returns {@code null}.
*
* @param oldItemPosition The position of the item in the old list
* @param newItemPosition The position of the item in the new list
*
* @return A payload object that represents the change between the two items.
*/
@Nullable
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return null;
}
}
DiffUtil.Callback和DiffUtil.ItemCallback不同,DiffUtil.Callback是用来比较两个列表的,DiffUtil.ItemCallback是用来比较两个Item的。
DiffUtil.DiffResult是列表比较的结果,它可以通过dispatchUpdatesTo把结果分发给Adapter。
触发DiffUtil#calculateDiff会返回DiffUtil.DiffResult。
简单来说就是,更新数据时的操作为:DiffUtil#calculateDiff -> dispatchUpdatesTo
DiffUtil的使用可以参考AsyncListDiffer的源码。
public void dispatchUpdatesTo(@NonNull final RecyclerView.Adapter adapter) {
dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
}
public final class AdapterListUpdateCallback implements ListUpdateCallback {
@NonNull
private final RecyclerView.Adapter mAdapter;
/**
* Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter.
*
* @param adapter The Adapter to send updates to.
*/
public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
mAdapter = adapter;
}
/** {@inheritDoc} */
@Override
public void onInserted(int position, int count) {
mAdapter.notifyItemRangeInserted(position, count);
}
/** {@inheritDoc} */
@Override
public void onRemoved(int position, int count) {
mAdapter.notifyItemRangeRemoved(position, count);
}
/** {@inheritDoc} */
@Override
public void onMoved(int fromPosition, int toPosition) {
mAdapter.notifyItemMoved(fromPosition, toPosition);
}
/** {@inheritDoc} */
@Override
public void onChanged(int position, int count, Object payload) {
mAdapter.notifyItemRangeChanged(position, count, payload);
}
}
可以看到dispatchUpdatesTo方法中把adapter的引用给到了AdapterListUpdateCallback,在AdapterListUpdateCallback的回调方法中触发了adapter的notifyItemXXX方法,也就是说DiffUtil通过比对差异,帮我们调用了adapter的notifyItemXXX方法,所以如果我们已知变化的位置,最好使用notifyItemXXX方法。
private class MyDiffAdapter: RecyclerView.Adapter<MyViewHolder>() {
private val mData = mutableListOf<AdapterType>()
private lateinit var onClickListener: OnClickListener;
fun setClickListener(listener: OnClickListener) {
onClickListener = listener
}
fun submitData(newData: List<AdapterType>) {
if (mData == newData) {
// 如果新旧列表一致,则直接返回
return
}
val diffResult = DiffUtil.calculateDiff(MyDiffCallback(mData, newData))
mData.clear()
mData.addAll(newData)
diffResult.dispatchUpdatesTo(this)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.adapter_item,
parent,
false
)
)
}
override fun getItemCount(): Int {
return mData.size
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
Log.d("MyDiffAdapter", "position: " + position)
holder.bind(mData[position], onClickListener)
}
}
private class MyDiffCallback(val oldData: List<AdapterType>, val newData: List<AdapterType>): Callback() {
override fun getOldListSize(): Int {
return oldData?.size ?: 0
}
override fun getNewListSize(): Int {
return newData?.size ?: 0
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldData[oldItemPosition]
val newItem = newData[newItemPosition]
return oldItem !== null && newItem !== null && oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldData[oldItemPosition]
val newItem = newData[newItemPosition]
return oldItem !== null && newItem !== null && oldItem.name == newItem.name
}
}
val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
//val adapter = MyListAdapter()
//val adapter = MyDifferAdapter()
val adapter = MyDiffAdapter()
adapter.setClickListener(OnClickListener {
var newList = mutableListOf<AdapterType>()
newList.addAll(adapters)
newList[1] = AdapterType(1, "My Adapter")
adapter.submitData(newList)
Toast.makeText(this, "click", Toast.LENGTH_SHORT).show() })
adapter.submitData(adapters)
recyclerView.adapter = adapter
可以看到提交数据时同样需要一个新的列表,否则无法比较两个列表的差异。
如果想要执行Item的局部刷新,需要用到DiffUtil.Callback#getChangePayload,当areItemsTheSame返回true,areContentsTheSame返回false时,DiffUtil会回调此方法获取变化的内容。
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val bundle = Bundle()
bundle.putString("name", newData[newItemPosition].name)
return bundle
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int, payloads: MutableList<Any>) {
Log.d("onBindViewHolder", "position: " + position)
if (payloads.isEmpty()) {
onBindViewHolder(holder, position)
} else {
val bundle: Bundle = payloads[0] as Bundle
holder.setName(bundle.getString("name", "0"))
}
}
在getChangePayload回调方法中,我们返回了一个包含要更新内容的Bundle,并重写了Adapter的onBindViewHolder(@NonNull VH holder, int position, @NonNull List<Object> payloads)方法,执行局部更新操作。
总结:
- 如果你已知更新位置,则不需要DiffUtil,只需重写notifyItemXXX方法即可;
- ListAdapter和AsyncListDiffer是DiffUtil的封装版本;
- 局部刷新需要重写getChangePayload;
- DiffUtil不止可以用在列表更新,还可以用在其他场景;
- 如果数据量特别大,建议放在子线程计算差异,放在主线程更新列表;
参考:
1、DiffUtil
2、将 DiffUtil 和数据绑定与 RecyclerView 结合使用
3、ListAdapter
4、AsyncListDiffer
网友评论