关于RecyclerView的更新
RecyclerView在显示静态的列表的数据的时候,我们用普通的Adapter,然后添加列表,调用notifyDataSetChanged()即可展示列表,但是对于动态变化的列表来说,全靠notifyDataSetChanged()来完成列表更新显得非常没有效率,因为有时候开发者只是想增删一个Item,而这却要付出刷新全部列表的代价。于是谷歌又给我们提供了多种api让我们完成局部Item的增删查改,如下:
- notifyItemRemoved()
- notifyItemInserted()
- notifyItemRangeChanged()
- ...
这些api固然好用但是对于某些场景来说我们难以下手,例如后台返回的列表的全部数据,在获取新的列表之后,开发者也许想比较新旧列表的不同,然后更新发生变化的item,这又如何实现呢?
关于DiffUtil
谷歌根据开发者需要比较新旧列表异同的痛点,推出了DiffUtil工具,它的核心算法是Myers差分算法,有兴趣可以自行学习,这篇文章不作深入探讨(其实笔者也不会)。
关于ListAdapter
注:这个ListAdapter是需要额外引入的,给RecyclerView使用的一个Adapter,并非SDK里面的那个,因此需要区分开来。
ListAdapter是谷歌基于上述的DiffUtil进行封装的一个Adapter,简单地继承重写即可达到DiffUtil的效果,高效完成RecyclerView的更新,这个也是本篇的重点。
实战
布局和对应的实体类
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <TextView android:id="@+id/tv_name" tools:text="名字" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:id="@+id/tv_age" tools:text="18岁" app:layout_constraintStart_toEndOf="@id/tv_name" app:layout_constraintTop_toTopOf="parent" android:layout_marginStart="20dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:id="@+id/tv_tall" tools:text="180cm" app:layout_constraintStart_toEndOf="@id/tv_age" app:layout_constraintTop_toTopOf="parent" android:layout_marginStart="20dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:id="@+id/tv_long" tools:text="18cm" app:layout_constraintStart_toEndOf="@id/tv_tall" app:layout_constraintTop_toTopOf="parent" android:layout_marginStart="20dp" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </androidx.constraintlayout.widget.ConstraintLayout>
data class ItemTestBean( val name:String, val age:Int, val tall:Int, val long:Int )
重写ListAdapter
ListAdapter的重写包含的关键点比较多,这里分步骤说明:
第一步:实现DiffUtil.ItemCallback
这是整个ListAdapter中最最最关键的一个步骤,因为它是DiffUtil知道如何正确修改列表的核心,我们直接看代码。
object ItemTestCallback : DiffUtil.ItemCallback<ItemTestBean>() { override fun areItemsTheSame(oldItem: ItemTestBean, newItem: ItemTestBean): Boolean { return oldItem.name == newItem.name } override fun areContentsTheSame(oldItem: ItemTestBean, newItem: ItemTestBean): Boolean { return oldItem.name == newItem.name && oldItem.age == newItem.age && oldItem.tall == newItem.tall && oldItem.long == newItem.long } }
乍一看非常复杂,实际原理非常简单,areItemsTheSame()方法判断的是实体类的主键,areContentsTheSame()方法判断的是实体类中会导致UI变化的字段。
第二步:实现viewHolder
这一步和其他的Adapter没什么区别,笔者用了viewBinding,你也可以根据自己项目实际情况改造。
inner class ItemTestViewHolder(private val binding: ItemTestBinding):RecyclerView.ViewHolder(binding.root){ fun bind(bean:ItemTestBean){ binding.run { tvName.text=bean.name tvAge.text=bean.age.toString() tvTall.text=bean.tall.toString() tvLong.text=bean.long.toString() } } }
第三步:组合成完整的ListAdapter
在ListAdapter中填入相应的泛型(实体类和ViewHolder类型),然后在构造函数中传入我们刚才实现的DiffUtil.ItemCallback即可,实现的两个方法和其他Adapter大同小异,唯一需要注意的是ListAdapter为我们提供了一个getItem的快捷方法,因此在onBindViewHolder()时可以直接调用。
class ItemTestListAdapter : ListAdapter<ItemTestBean,ItemTestListAdapter.ItemTestViewHolder>(ItemTestCallback) { inner class ItemTestViewHolder(private val binding: ItemTestBinding):RecyclerView.ViewHolder(binding.root){ //...省略 } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemTestViewHolder { return ItemTestViewHolder(ItemTestBinding.inflate(LayoutInflater.from(parent.context),parent,false)) } override fun onBindViewHolder(holder: ItemTestViewHolder, position: Int) { //通过ListAdapter内部实现的getItem方法找到对应的Bean holder.bind(getItem(position)) } }
使用ListAdapter完成列表的增删查改
为了方便演示,使用如下的List和初始化代码:
private val testList = listOf<ItemTestBean>( ItemTestBean("小明",18,180,18), ItemTestBean("小红",19,180,18), ItemTestBean("小东",20,180,18), ItemTestBean("小刘",18,180,18), ItemTestBean("小德",15,180,18), ItemTestBean("小豪",14,180,18), ItemTestBean("小江",12,180,18), ) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding=ActivityMainBinding.inflate(LayoutInflater.from(this)) setContentView(binding.root) val adapter=ItemTestListAdapter() binding.rv.adapter=adapter }
插入元素
插入全新的列表
adapter.submitList(testList)
完事了??
是的,我们只需要调用submitList方法告诉Adatper我们要插入一个新的列表即可。
局部插入元素
也许插入全新列表并不能让你感觉到ListAdapter的精妙之处,因为这和原来的Adapter差别并不大,我们再来试试往列表中插入局部的元素,例如我们要在小刘和小德之间插入一个新的Item。
我们对列表转成可变列表(为什么使用不可变列表,原因后面会解释),然后插入元素,最后调用submitList把新的列表传入进去即可。
val newList=testList.toMutableList().apply { add(3,ItemTestBean("坤坤鸡",21,150,4)) } adapter.submitList(newList)
列表更新了,由此可见,无论是增加一个元素还是多个元素,我们都只需要调submitList即可。
这里说一下为什么要重新传入一个新的List而不是对原来的List进行修改,因为源码中有这样一段,笔者推测是因为这个校验差分的逻辑是异步的,如果外部对原列表进行修改会导致内部的逻辑异常(未验证只是猜测)。
因此我们切记要传入新的List而不是对原List进行修改。
public void submitList(@Nullable final List<T> newList, @Nullable final Runnable commitCallback) { //...省略 //校验列表是否是同一个对象 if (newList == mList) { // nothing to do (Note - still had to inc generation, since may have ongoing work) if (commitCallback != null) { commitCallback.run(); } return; } //...省略 }
删除元素和修改元素
聪明的读者估计也已经猜到了,无论是增加删除和修改,我们都只需要submitList即可,因为List中就已经包含了列表的更新信息,一切的更新ListAdapter已经自动替我们完成了。
val newList=testList.toMutableList().apply { //删除 removeAt(2) //修改 this[3]=this[3].copy(name = "改名后的小帅哥") } adapter.submitList(newList)
列表清空
一切尽在submitList,如果我们要让列表清空,那我们就submit一个空对象就行了,非常简单!
adapter.submitList(null)
使用新的列表进行更新(项目中最常见的复杂场景)
val newList=listOf( //修改 ItemTestBean("小明",18,20,18), ItemTestBean("小红",19,180,18), //插入 ItemTestBean("蔡徐鸡",20,180,18), ItemTestBean("小刘",18,180,18), ItemTestBean("我爱你",14,180,18), ItemTestBean("小江",12,180,18), ) adapter.submitList(newList)
我们可以看到,新的列表相对原列表而言,发生了修改、删除、插入等操作,如果这些由开发者自己来维护,是非常麻烦的,但是依靠ListAdapter内置的差异性算法,自动帮我们完成了这些工作。
总结
笔者使用一个简单的案例演示了ListAdapter如何帮助开发者完成列表差异性更新的逻辑,非常适合那些返回整段列表然后更新局部元素的逻辑,例如后台返回的一整段列表,这些列表可能只有一两个元素发生了变化,如果按照传统的notifyDataSetChange()会严重浪费性能,而ListAdapter只会更新那些发生了变化的区域。
如果你的项目不能直接使用ListAdapter,也希望使用这个差分算法,你可以直接使用DiffUtil去更新你项目的Adapter,关于这个DiffUtil的直接使用,网上有许多教程,用起来也并不难,这里不在赘述。
喜欢请点赞关注,你的支持是笔者更新的动力。