关于RecyclerView的更新

  RecyclerView在显现静态的列表的数据的时候,咱们用普通的Adapter,然后添加列表,调用notifyDataSetChanged()即可展现列表,可是关于动态改变的列表来说,全靠notifyDataSetChanged()来完结列表更新显得十分没有效率,因为有时候开发者仅仅想增删一个Item,而这却要支付改写悉数列表的代价。于是谷歌又给咱们供给了多种api让咱们完结部分Item的增删查改,如下:

  1. notifyItemRemoved()
  2. notifyItemInserted()
  3. 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>

回绝手动Notifydatasetchanged(),运用ListAdapter高效完结RecyclerView改写

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)

回绝手动Notifydatasetchanged(),运用ListAdapter高效完结RecyclerView改写

完事了??

  是的,咱们只需求调用submitList办法告知Adatper咱们要刺进一个新的列表即可。

回绝手动Notifydatasetchanged(),运用ListAdapter高效完结RecyclerView改写

部分刺进元素

  或许刺进全新列表并不能让你感觉到ListAdapter的精妙之处,因为这和本来的Adapter不同并不大,咱们再来试试往列表中刺进部分的元素,例如咱们要在小刘和小德之间刺进一个新的Item。

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

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

回绝手动Notifydatasetchanged(),运用ListAdapter高效完结RecyclerView改写

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

回绝手动Notifydatasetchanged(),运用ListAdapter高效完结RecyclerView改写

咱们能够看到,新的列表相对原列表而言,发生了修正、删去、刺进等操作,假如这些由开发者自己来维护,是十分麻烦的,可是依靠ListAdapter内置的差异性算法,主动帮咱们完结了这些作业。

总结

  笔者运用一个简略的案例演示了ListAdapter怎么协助开发者完结列表差异性更新的逻辑,十分合适那些回来整段列表然后更新部分元素的逻辑,例如后台回来的一整段列表,这些列表或许只有一两个元素发生了改变,假如依照传统的notifyDataSetChange()会严峻糟蹋性能,而ListAdapter只会更新那些发生了改变的区域。

  假如你的项目不能直接运用ListAdapter,也期望运用这个差分算法,你能够直接运用DiffUtil去更新你项目的Adapter,关于这个DiffUtil的直接运用,网上有许多教程,用起来也并不难,这儿不在赘述。

喜欢请点赞重视,你的支撑是笔者更新的动力。