天天看点

DiffUtil工具类使用-让recyclerview使用更高效DiffUtil工具类使用-让recyclerview使用更高效

DiffUtil工具类使用-让recyclerview使用更高效

问题背景

安卓开发过程中,recyclerview是很常见的滑动列表视图组件,数据刷新的时候,我们经常就是直接调用了mAdapter.notifyDataSetChanged()的方法进行操作。但是很显然,这样直接操作有两个问题:

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

(2)性能较低,毕竟是无脑的刷新了一遍整个RecyclerView。

问题分析

这时候,就有一个好用的工具类登场了。DiffUtil是support-v7:24.2.0中的新工具类,它用来比较两个数据集,寻找出旧数据集到新数据集的最小变化量。使用DiffUtil后,改为如下代码:

// 新方案
val diffResult = DiffUtil.calculateDiff(DiffUtilCallBack(oldData, mDatas), true)
diffResult.dispatchUpdatesTo(mAdapter!!)
           

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

adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
adapter.notifyItemRangeChanged(position, count, payload);

           

显然,这个四个方法在执行时都是伴有RecyclerView的动画的,且都是定向刷新方法,效率明显是会提升不少。

实践demo

(1)新建一个JavaBean类,列表item的数据,代码如下:

class MyBean(var name: String, var desc: String, var pic: Int) :
    Cloneable {

    @Throws(CloneNotSupportedException::class)
    public override fun clone(): MyBean {
        var bean: MyBean? = null
        try {
            bean = super.clone() as MyBean
        } catch (e: CloneNotSupportedException) {
            e.printStackTrace()
        }
        return bean!!
    }
}
           

(2)实现recyclerview对应的adapter,代码如下:

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import composer.model.MyBean


class DiffAdapter(private val mContext: Context, mDatas: List<MyBean>?) :
    RecyclerView.Adapter<DiffAdapter.DiffViewHolder>() {
    private var mDatas: List<MyBean>?
    private val mInflater: LayoutInflater

    init {
        this.mDatas = mDatas
        mInflater = LayoutInflater.from(mContext)
    }

    fun setDatas(mDatas: List<MyBean>?) {
        this.mDatas = mDatas
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DiffViewHolder {
        return DiffViewHolder(mInflater.inflate(mContext.resources.getLayout(R.layout.item_diff1), parent, false))
    }

    override fun onBindViewHolder(holder: DiffViewHolder, position: Int) {
        val bean: MyBean = mDatas!![position]
        holder.tv1.text = bean.name
        holder.tv2.text = bean.desc
        holder.iv.setImageResource(bean.pic)
    }

    override fun getItemCount(): Int {
        return if (mDatas != null) mDatas!!.size else 0
    }

    inner class DiffViewHolder(itemView: View) :
        RecyclerView.ViewHolder(itemView) {
        var tv1: TextView
        var tv2: TextView
        var iv: ImageView

        init {
            tv1 = itemView.findViewById(R.id.text1)
            tv2 = itemView.findViewById(R.id.text2)
            iv = itemView.findViewById<ImageView>(R.id.img1)
        }
    }

    companion object {
        private const val TAG = "DiffAdapter"
    }
}
           

(3)item对应的layout布局文件,R.layout.item_diff代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:orientation="horizontal"
    android:layout_height="wrap_content">
    <TextView
        android:id="@+id/text1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:id="@+id/text2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <ImageView
        android:id="@+id/img1"
        android:layout_width="50dp"
        android:layout_height="wrap_content"/>
</LinearLayout>
           

(4)新建activity代码如下:

package composer

import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import composer.adapter.DiffAdapter
import composer.model.MyBean


class DiffUtilActivity : AppCompatActivity() {
    private var mDatas: MutableList<MyBean>? = null
    private var mRv: RecyclerView? = null
    private var mAdapter: DiffAdapter? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_diff_util)

        initData()
        mRv = findViewById(R.id.recyclerView1)

        mRv?.layoutManager = LinearLayoutManager(this)
        mAdapter = DiffAdapter(this, mDatas)
        mRv?.adapter = mAdapter
    }

    private fun initData() {
        mDatas = mutableListOf()
        mDatas?.add(MyBean("测试1", "Android", R.drawable.bg_ranking))
        mDatas?.add(MyBean("测试2", "Java", R.drawable.ic_pic_add))
        mDatas?.add(MyBean("测试3", "C", R.drawable.abc_vector_test))
        mDatas?.add(MyBean("测试4", "PHP", R.drawable.bg_daily_btn))
        mDatas?.add(MyBean("测试5", "Python", R.drawable.bg_live_top))
    }

    /**
     * 模拟刷新操作
     *
     * @param view
     */
    fun onRefresh(view: View?) {
        try {
            mDatas?.add(MyBean("李小龙", "帅", R.drawable.bg_live_bottom))
            val myBean = mDatas?.removeAt(1)
            if (myBean != null) {
                mDatas?.add(myBean)
            }
            mAdapter?.notifyDataSetChanged()
        } catch (e: CloneNotSupportedException) {
            e.printStackTrace()
        }
    }
}
           

(5)activity对应的layout布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".activity.diffUtilTest.DiffUtilActivity"
    android:orientation="vertical"
    tools:ignore="MissingDefaultResource">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <LinearLayout
            android:layout_width="match_parent"
            android:orientation="vertical"
            android:layout_height="wrap_content">
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/recyclerView1"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>
            <Button
                android:id="@+id/btnRefresh"
                android:text="模拟刷新"
                android:onClick="onRefresh"
                android:layout_gravity="center_horizontal"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>
        </LinearLayout>
    </ScrollView>
</LinearLayout>
           

demo分析

我们在activity中模拟数据修改,通知recyclerview适配数据修改,使用的方法是:

mAdapter.notifyDataSetChanged();
           

这个方法,基本属于全局重新刷新数据,Android studio也会提示

DiffUtil工具类使用-让recyclerview使用更高效DiffUtil工具类使用-让recyclerview使用更高效

DiffUtil优化方案

import androidx.recyclerview.widget.DiffUtil;
import java.util.List;

public class DiffUtilCallBack extends DiffUtil.Callback {
    private List<MyBean> mOldDatas;
    private List<MyBean> mNewDatas;

    public DiffUtilCallBack(List<MyBean> mOldDatas, List<MyBean> mNewDatas) {
        this.mOldDatas = mOldDatas;
        this.mNewDatas = mNewDatas;
    }

    /**
     * 老数据集size
     */
    @Override
    public int getOldListSize() {
        return mOldDatas != null ? mOldDatas.size() : 0;
    }

    /**
     * 新数据集size
     */
    @Override
    public int getNewListSize() {
        return mNewDatas != null ? mNewDatas.size() : 0;
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return mOldDatas.get(oldItemPosition).getName().equals(mNewDatas.get(newItemPosition).getName());
    }

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        MyBean beanOld = mOldDatas.get(oldItemPosition);
        MyBean beanNew = mNewDatas.get(newItemPosition);
        if (!beanOld.getDesc().equals(beanNew.getDesc())) {
            // 如果有内容不同,就返回false
            return false;
        }
        if (beanOld.getPic() != beanNew.getPic()) {
            // 如果有内容不同,就返回false
            return false;
        }
        // 默认两个data内容是相同的
        return true;
    }
}


           
package composer

import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import composer.adapter.DiffAdapter
import composer.callback.DiffUtilCallBack
import composer.model.MyBean


class DiffUtilActivity : AppCompatActivity() {
    private var mDatas: MutableList<MyBean>? = null
    private var mRv: RecyclerView? = null
    private var mAdapter: DiffAdapter? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_diff_util)

        initData()
        mRv = findViewById(R.id.recyclerView1)

        mRv?.layoutManager = LinearLayoutManager(this)
        mAdapter = DiffAdapter(this, mDatas)
        mRv?.adapter = mAdapter
    }

    private fun initData() {
        mDatas = mutableListOf()
        mDatas?.add(MyBean("测试1", "Android", R.drawable.bg_ranking))
        mDatas?.add(MyBean("测试2", "Java", R.drawable.ic_pic_add))
        mDatas?.add(MyBean("测试3", "C", R.drawable.abc_vector_test))
        mDatas?.add(MyBean("测试4", "PHP", R.drawable.bg_daily_btn))
        mDatas?.add(MyBean("测试5", "Python", R.drawable.bg_live_top))
    }

    /**
     * 模拟刷新操作
     *
     * @param view
     */
    fun onRefresh(view: View?) {
        try {
            val oldData = mutableListOf<MyBean>()
            mDatas?.let { oldData.addAll(it) }

            mDatas?.add(MyBean("李小龙", "帅", R.drawable.bg_live_bottom))
            val myBean = mDatas?.removeAt(1)
            if (myBean != null) {
                mDatas?.add(myBean)
            }
            // 之前的方案
//            mAdapter?.notifyDataSetChanged()
            // 新方案
            val diffResult = DiffUtil.calculateDiff(DiffUtilCallBack(oldData, mDatas), true)
            diffResult.dispatchUpdatesTo(mAdapter!!)
        } catch (e: CloneNotSupportedException) {
            e.printStackTrace()
        }
    }
}