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

简介: 拒绝手动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的直接使用,网上有许多教程,用起来也并不难,这里不在赘述。

喜欢请点赞关注,你的支持是笔者更新的动力。


相关文章
|
Android开发
Android RecyclerView的notify方法和动画的刷新详解(二)
Android RecyclerView的notify方法和动画的刷新详解
292 0
|
XML Android开发 数据格式
Android RecyclerView的notify方法和动画的刷新详解(一)
Android RecyclerView的notify方法和动画的刷新详解
194 0
|
消息中间件 存储 缓存
RecyclerView 刷新列表数据的 notifyDataSetChanged() 为什么是昂贵的?
RecyclerView 刷新列表数据的 notifyDataSetChanged() 为什么是昂贵的?
74 0
|
存储 缓存 开发工具
RecyclerView#Adapter#notifyDataSetChanged方法后,为何还会新建ViewHolder?
RecyclerView#Adapter#notifyDataSetChanged方法后,为何还会新建ViewHolder?
RecyclerView#smoothScrollToPosition调用RecyclerView#OnScrollListener的过程
项目中使用到了RecyclerView#smoothScrollToPosition(0)方法让Recyclerview滚动到顶部,同时给Recyclerview设置了监听器RecyclerView.OnScrollListener。
RecyclerView学习-RecyclerView#Adapter#notifyDataSetChanged是如何更新数据的?
RecyclerView学习-RecyclerView#Adapter#notifyDataSetChanged是如何更新数据的?
|
存储 缓存 算法
更高效地刷新 RecyclerView | DiffUtil二次封装
每次数据变化都全量刷新整个列表是很奢侈的,不仅整个列表会闪烁一下,而且所有可见表项都会重新绑定一遍数据。这一篇对 DiffUtil 进行二次封装以让其更易于使用。
585 0
|
API 开发工具 git
使用RecycleView优雅的实现数据列表更新
使用RecycleView优雅的实现数据列表更新
646 0
使用RecycleView优雅的实现数据列表更新
|
存储 缓存 算法
读源码长知识 | 更好的 RecyclerView 表项点击监听器
RecyclerView没有提供表项点击事件监听器,只能自己处理。这一篇介绍一种更加解耦,更易于使用的表项点击事件监听方法。
213 0
|
数据可视化 Android开发 索引
【RecyclerView】 十三、RecyclerView 数据更新 ( 移动数据 | 数据改变 )
【RecyclerView】 十三、RecyclerView 数据更新 ( 移动数据 | 数据改变 )
560 0
【RecyclerView】 十三、RecyclerView 数据更新 ( 移动数据 | 数据改变 )