美文网首页
RecyclerView中的DiffUtil

RecyclerView中的DiffUtil

作者: 慎独静思 | 来源:发表于2023-04-14 16:27 被阅读0次

RecyclerView相关的文章预计会写六篇,此处是第二篇

  1. RecyclerView中的position
  2. RecyclerView中的DiffUtil
  3. RecyclerView中的SnapHelper
  4. RecyclerView中的Selection
  5. RecyclerView中的ConcatAdapter
  6. RecyclerView中的Glide预加载

是什么?

DiffUtil是一个工具类,用来处理列表更新时的差异,计算出更新的部分,输出给RecyclerView来只刷新更新的部分,提高刷新效率。
平时我们开发过程中如果列表内容或列表项发生变化,我们会触发 notifyDataSetChanged更新列表,但 notifyDataSetChanged方法效率低下,它会告知RecyclerView整个列表已经无效,
导致整个列表重新绑定和重绘,包括列表中看不到的项,当列表数据过大时,可能会带来闪烁或卡顿问题,影响用户体验。
一种解决办法是使用notifyItemxxx方法,只更新RecyclerView中变化的内容,对于一些简单场景且已知Item位置的情况下可以使用此种方式,很多情况下我们无法确切直到要更新Item的位置,这时候就需要DiffUtil出场了。

在介绍DiffUtil之前,我们先介绍一下它的两个孪生兄弟 ListAdapterAsyncListDiffer
此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

小结

  1. ListAdapter构造方法中传入了一个DiffUtil.ItemCallback,并重写了areItemsTheSame和areContentsTheSame来判断是否相同Item;
  2. 在获取Item时需要使用getItem(position),不能直接从数据中获取对应Item;
  3. 更新数据(增,删,改)时需要调用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

小结

  1. 操作List的数据需要通过mDiffer.currentList完成,不能直接访问列表数据;
  2. 更新列表需要通过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)方法,执行局部更新操作。

总结:

  1. 如果你已知更新位置,则不需要DiffUtil,只需重写notifyItemXXX方法即可;
  2. ListAdapter和AsyncListDiffer是DiffUtil的封装版本;
  3. 局部刷新需要重写getChangePayload;
  4. DiffUtil不止可以用在列表更新,还可以用在其他场景;
  5. 如果数据量特别大,建议放在子线程计算差异,放在主线程更新列表;

参考:
1、DiffUtil
2、将 DiffUtil 和数据绑定与 RecyclerView 结合使用
3、ListAdapter
4、AsyncListDiffer

相关文章

网友评论

      本文标题:RecyclerView中的DiffUtil

      本文链接:https://www.haomeiwen.com/subject/urqeddtx.html