从 notifyDataSetChanged 到 DiffUtil

封面图

DiffUtil 是 RecyclerView 中自带的一个计算数据集差异性的组件,其内部还提供了对 RecyclerView 进行局部刷新的 API。以往我们对 RecyclerView 的刷新,大多会采用 notifyDataSetChanged,而这种刷新方式,会造成 RecyclerView#Adapter 调用 List#size 次的 onCreateViewHolder 和 onBindViewHolder,很容易就造成资源浪费,在使用了 DiffUtil 之后,无论是添加、删除、还是更新元素,也只会调用对应数量个 onCreateViewHolder 和 onBindViewHolder,从而对 RecyclerView 的使用进行优化。

下面我们就通过一个简单的例子来认识一下这个 DiffUtil。

创建 RecyclerView

首先,我们创建出一个简单的列表。然后为其设置 Adapter、LayoutManager 等 RecyclerView 基础配置。

代码如下:

private val numberList = mutableListOf<SimpleText>()
private val simpleAdapter = SimpleAdapter(numberList)

rv_main_diff.adapter = simpleAdapter
rv_main_diff.layoutManager = LinearLayoutManager(this)
rv_main_diff.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))

for (i in 1..10) {
  numberList.add(SimpleText("Tittle $i"))
}

prepareList()
simpleAdapter.notifyDataSetChanged()

data class SimpleText(var title: String)

SimpleAdapter 和 布局文件内容比较简单,就不展示了,仅仅是把 SimpleText 的中的 title 显示到 RecyclerView 的 Item 上。

image

更新列表

实现 DiffUtil.Callback

在没有使用 DiffUtil 时,更新页面上的内容我们需要更新数据集合「List」,然后再次调用 Adapter#notifyDataSetChanged 或 Adapter 的其它局部更新的方法进行刷新即可。现在我们既然要使用 DiffUtill,那么具体该怎么做呢?

首先,我们需要创建一个 DiffUtil.Callback 的实现类,DiffUtil.Callback 是一个抽象类,其中包含了 4 个抽象方法,都是用于比较两个数据集时所必须用到的方法。

class DiffCallback(private val oldList: List<SimpleText>, private val newList: List<SimpleText>) : DiffUtil.Callback() {

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition] === newList[newItemPosition]
    }

    override fun getOldListSize(): Int {
        return oldList.size
    }

    override fun getNewListSize(): Int {
        return newList.size
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].title.hashCode() == newList[newItemPosition].title.hashCode()
    }
}

DiffUtil.Callback 需要两个列表,就把新旧两个数据集合在构造函数中传入即可。

  • getOldListSize:顾名思义就是获取上一个版本的列表个数;
  • getNewListSize:和 getOldListSize 一样,只不过是获取最新的列表个数;
  • areItemsTheSame:这个方法是用于比较指定新旧两个元素位置所对应的元素是否为同一个元素;
  • areContentsTheSame:这个方法有些类似上一个,区别在于确定两个元素是同一个元素后,判断新旧列表中的这两个元素内容是否相同。

到这里,我们猜一下,这个 DiffUtil 的工作原理:先进行判断新旧两个数据集的尺寸是否相同,然后再比较新旧两个数据集同一个下标的所对应的元素是否相同,最后再对元素内容是否一致进行比较。

不管猜的对不对,我们先继续。

在创建完 DiffUtil.Callback 的实现类后,需要对它进行实例化,注意看,DiffCallback 的主构造器声明了两个字段,所以我们在对它进行实例化时也需要传入两个元素。

编辑列表元素

现在我们有个需求:对数据集合中倒数第二个元素进行编辑。我们该怎么做?按照之前不使用 Diff 的时候,我们应该这么做:

numberList[numberList.size - 2].title = "New Edit To"
simpleAdapter.notifyItemChanged(numberList.size - 2)

现在呢?自然是用 DiffUtil.Callback 来进行,不过在使用 Diff 之前,我们还需要准备一个 List。因为我们刚才给 DiffCallback 的主构造器声明了两个字段,一个是 oldList,一个是 newList。oldList 就是我们的 numberList,我们还需要准备一个新的 List。

val tmpList = MutableList<SimpleText>(10) {
  return@MutableList numberList[it]
}
tmpList[tmpList.size - 2] = SimpleText("New Edit To")

数据准备完成后,接下来就是应用在 Diff 上了。

val diffResult = DiffUtil.calculateDiff(DiffCallback(numberList, tmpList))
numberList.clear()
numberList.addAll(tmpList)
diffResult.dispatchUpdatesTo(simpleAdapter)

可以看到,就这样很简单的把两个 List 放在 DiffUtil#calculateDiff 中进行计算,然后得出 diffResult,最后将 diffResult 关联到 Adapter 即可。

怎么样?是不是挺简单的?去试一下,向列表添加元素和删除元素吧。

添加和删除元素本质上和编辑元素没太多区别,就不进行阐述了,在提供的代码中都有。

image

ListAdapter

Diff 用起来好像挺简单的,不过 Google 还向我们提供了一个工具,能让我们用起来更方便,这个就是 ListAdapter。

ListAdapter 继承了 RecyclerView#Adapter,并且开发者不需要再实现 DiffCallback 的四个方法,只需要实现其中的两个「areItemsTheSame,areContentsTheSame」即可。也不需要调用拿到 DiffUtil#calculateDiff 的结果了。只需要在需要更新列表的时候,调用 simpleListAdapter.submitList(List) 即可,不过 RecyclerView 的 Adapter 就不能继承 RecyclerView#Adaper 了,而是需要继承自 ListAdapter。

class SimpleDiffItemCallback : DiffUtil.ItemCallback<MainActivity.SimpleText>() {
    override fun areItemsTheSame(oldItem: MainActivity.SimpleText, newItem: MainActivity.SimpleText): Boolean {
        return oldItem == newItem
    }

    override fun areContentsTheSame(oldItem: MainActivity.SimpleText, newItem: MainActivity.SimpleText): Boolean {
        return oldItem.title.hashCode() == newItem.title.hashCode()
    }
}

class SimpleListAdapter : ListAdapter<MainActivity.SimpleText, SimpleListViewHolder>(SimpleDiffItemCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleListViewHolder {
        return SimpleListViewHolder(LayoutInflater.from(parent.context).inflate(android.R.layout.simple_list_item_1, parent,false))
    }

    override fun onBindViewHolder(holder: SimpleListViewHolder, position: Int) {
        holder.tvText.text = getItem(position).title
    }
}

class SimpleListViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
    var tvText: TextView = itemView.findViewById(android.R.id.text1)
}

仔细看的朋友可能会发现,我在使用 ListAdapter 时传入一个 SimpleDiffItemCallback 这个对象,这个对象实现的抽象类是 DiffUtil.ItemCallback。这个 ItemCallback 和之前使用 Callback 有 3 处不同。

  1. Callback 是 DiffUtil 用于比较两个列表,而 ItemCallback 是 DiffUtil 用于比较两个元素不为空的列表;
  2. Callback 有 4 个抽象方法,ItemCallback 只有 2 个;
  3. Callback 适用于自己创建并应用在 RecyclerView#Adapter 上,ItemCallback 是 ListAdapter 内置在其中的。

怎么样?ListAdapter 是不是挺简单的?不过我现在有点好奇,ListAdpater 内部具体做了哪些事情?来让我们看看 ListAdapter 的源码。

ListAdapter 到底干了啥

当我点开 ListAdapter 的源码时,我发现,它内部好像啥也没干,又好像啥都干了。具体来说就是把全部的脏活累活都丢给了 AsyncListDiffer 这个类,ListAdapter 自己就像是一个代理,需要做什么,就丢给 AsyncListDiffer 这个类,需要获取什么就从 AsyncListDiffer 这个类里取出来。

image

看到这,我继续进入 AsyncListDiffer,发现这个类其实也没特别多的东西,最重要的就是其中两个方法。一个是 submitList,另一个是 latchList。

submitList 其实就是我们在 Activity/Fragment 调用 adapter#submit(List) 后通过各种调用最终的目的地。不过在 submitList 的内部实际上是通过在子线程计算两个列表的差异,并在主线程上通过调用 latchList 将数据关联到 RecyclerView#Adapter 的效果。

latchList 做的内容就很简单了,只是将数据进行绑定。

image

多类型怎么做?

在这个例子中,我使用的是单一类型的数据集,但是在实际项目中存在多类型数据集该怎么做呢?我这里提供一下思路和简单的示例代码。

思路:

  1. 多类型数据集合,我们可以创建一个 Base 类,在 Base 类中定义类型,不同的类型在继承 Base 类时,在其构造方法内调用 Base 的构造方法,并传入类型。
  2. 如果不使用 ListAdapter 的话,RecyclerView#Adapter 就可以按照以往的多类型编写方式进行,只需要更改 DiffUtil.Callback 的实现类中的 areItemsTheSame 和 areContentsTheSame 的具体实现。具体实现可以先判断元素类型是否相同,然后再对内容进行判断;
  3. 如果使用 ListAdapter 的话,需要在继承的 ListAdapter 传入泛型进行更改,而且还需要对实现 DiffUtil.ItemCallback 的实现类的传入泛型的更改;

怎么样?Diff 用起来很简单吧?要不要去试一下?而且,偷偷告诉你,Google 发布的另一个库 Paging 内部也有 Diff 的身影哦。

本文示例代码已经上传到了 GitHub,分支为 diff多类型示例

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345