注册

拒绝手动Notifydatasetchanged(),使用ListAdapter高效完成RecyclerView刷新

关于RecyclerView的更新

  RecyclerView在显示静态的列表的数据的时候,我们用普通的Adapter,然后添加列表,调用notifyDataSetChanged()即可展示列表,但是对于动态变化的列表来说,全靠notifyDataSetChanged()来完成列表更新显得非常没有效率,因为有时候开发者只是想增删一个Item,而这却要付出刷新全部列表的代价。于是谷歌又给我们提供了多种api让我们完成局部Item的增删查改,如下:

  1. notifyItemRemoved()
  2. notifyItemInserted()
  3. notifyItemRangeChanged()
  4. ...

  这些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>

image.png

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)

image.png

完事了??

  是的,我们只需要调用submitList方法告诉Adatper我们要插入一个新的列表即可。

image.png

局部插入元素

  也许插入全新列表并不能让你感觉到ListAdapter的精妙之处,因为这和原来的Adapter差别并不大,我们再来试试往列表中插入局部的元素,例如我们要在小刘和小德之间插入一个新的Item。

  我们对列表转成可变列表(为什么使用不可变列表,原因后面会解释),然后插入元素,最后调用submitList把新的列表传入进去即可。

val newList=testList.toMutableList().apply {
add(3,ItemTestBean("坤坤鸡",21,150,4))
}
adapter.submitList(newList)

image.png

  列表更新了,由此可见,无论是增加一个元素还是多个元素,我们都只需要调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)

image.png

我们可以看到,新的列表相对原列表而言,发生了修改、删除、插入等操作,如果这些由开发者自己来维护,是非常麻烦的,但是依靠ListAdapter内置的差异性算法,自动帮我们完成了这些工作。

总结

  笔者使用一个简单的案例演示了ListAdapter如何帮助开发者完成列表差异性更新的逻辑,非常适合那些返回整段列表然后更新局部元素的逻辑,例如后台返回的一整段列表,这些列表可能只有一两个元素发生了变化,如果按照传统的notifyDataSetChange()会严重浪费性能,而ListAdapter只会更新那些发生了变化的区域。

  如果你的项目不能直接使用ListAdapter,也希望使用这个差分算法,你可以直接使用DiffUtil去更新你项目的Adapter,关于这个DiffUtil的直接使用,网上有许多教程,用起来也并不难,这里不在赘述。


作者:晴天小庭
链接:https://juejin.cn/post/7125275626134585352
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册