前言
隨着業務的急劇擴張,一些架構上的調整也隨之破土動工。從最初的MVC,管他是唱、跳、Rap,還是打籃球。通通寫在Activity裏;再到MVP階段的業務與View分離;然後就是現在的MVVM。
關於MVVM的內容,可以在我之前的文章中看到:
一點點入坑JetPack:實戰前戲NetworkBoundResource篇
我猜可能有小夥伴們會不解,上文一頓瞎BB,和題目中的函數式編程、DiffUtil又有啥關係呢。不要着急,這一整個系列將承接上一個MVVM系列,圍繞函數式編程徹底展開一個從業務層面往思想層面理解的一個過程。
這篇系列融合了很多公司大佬們的架構分享,加上我自己思考總結的一篇文章。希望可以給各位小夥伴們帶來收穫,當然也歡迎大家各抒己見,閉門造車就太不real了。
正文
作爲系列開篇第一章,我打算搞點實戰意義比較強的。所以這篇文章不會上來就扯思想上的東西,而是主要以DiffUtil的用法爲主。
主要包括以下部分:
- 1、notifyDataSetChanged()
- 2、DiffUtil基本用法
- 3、DiffUtil的思考
- 番外篇:源碼分析
閒話就不多說了,咱們搞快點、搞快點!
一、notifyDataSetChanged()
我相信這個方法,咱們大家都不陌生吧。在那個“懵懂無知”的編程初期,不知道有多少小夥伴和我一樣,靠着notifyDataSetChanged()
,一招鮮吃遍天下。
直到後來發現,數據量多了之後,notifyDataSetChanged()
變得巨慢無比…此時的自己只能“不滿”的噴一下Google:你tm就不能優化一下麼?…
直到自己瞭解到了notifyItemChanged()
、notifyItemInserted()
等方法的時候。才知道就算自己“編程時長兩年半”,該“蔡”還是“蔡”…
剛剛提到的那些方法,其實作爲“職場老司機”,我猜大家應該很熟悉它們的用法。當然還有Payload機制下的局部bindData()
。
關於傳統RecyclerView
的用法,就不多費口舌了,畢竟都是些基本操作。接下來就讓咱們走進DiffUtil
:
二、DiffUtil基本用法
從名字中,我們很容易猜到它的作用:一個幫我們diff數據集的工具。
對於之前的我們來說,diff的操作,都是我們業務方自己去處理的問題。然後根據數據的變動,自行選擇使用什麼樣的notify方法。
而DiffUtil就是幫我們做這部分內容,然後根據我們的具體實現,自動幫我們去notify。直接裸上代碼,畢竟能點進來的小夥伴,技術實力都不會太差。想要使用DiffUtil,第一步是繼承特定的接口:
2.1、繼承DiffUtil.Callback
先定一個數據結構:
// 不要在意這些變量是啥意思,就是3個不同的變量
data class Book(val id: Long, val name: String, val version: Long)
然後就是DiffUtil.Callback
的實現類:
class BooksDiffCallback : DiffUtil.Callback() {
private var oldData = emptyList<Book>()
private var data = oldData
fun update(data: List<Book>) {
oldData = this.data
this.data = data
}
// 如果此方法返回true,說明來個數據集中同一個position位置的數據沒有變化,至於如何notify需要參考areContentsTheSame()的返回值
// 如果此方法返回false,直接刷新item
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldData[oldItemPosition]
val newItem = data[newItemPosition]
return oldItem === newItem || oldItem.id == newItem.id
}
// 此方法會在areItemsTheSame()返回true的時候調用。
// 如果返回true,則意味着數據一樣,Item也一樣,不需要刷新。(這裏屬於業務方自行實現,比如我的實現就是當Book的version相同時,業務上認爲數據相同不需要刷新)
// 如果返回false,則意味着數據不同,需要刷新。不過這裏還有一個分支,那就是是否Payload。此時便會走到getChangePayload()中
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldData[oldItemPosition]
val newItem = data[newItemPosition]
return oldItem.version == newItem.version
}
// 這裏就是普通Payload的方法,當version不同且name不同時,我們就告訴DiffUtil使用PAYLOAD_NAME作爲Payload的表示
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val list = mutableListOf<Any>()
val oldItem = oldData[oldItemPosition]
val newItem = data[newItemPosition]
if (oldItem.name != newItem.name) {
list.add(PAYLOAD_NAME)
return list
}
companion object {
val PAYLOAD_NAME = Any()
}
override fun getOldListSize(): Int {
return oldData.size
}
override fun getNewListSize(): Int {
return data.size
}
}
完成這一步,我們就可以set我們的數據集了。
2.2、調用
我們可以在Adapter中簡單的封裝一個方法:
class BooksAdapter :RecyclerView.Adapter...{
private val diffCallback = BooksDiffCallback()
// ...省略部分代碼
// BookViewHolder就是一個普通的ViewHolder
override fun onBindViewHolder(viewHolder: BookViewHolder, book: Book) {
viewHolder.bindData(book, viewHolder.adapterPosition)
}
override fun onBindViewHolder(holder: BookViewHolder, item: Book, payloads:MutableList<Any>) {
if (payloads.isNullOrEmpty()) {
onBindViewHolder(holder, item)
return
}
if(payloads.contains(PAYLOAD_NAME)){
// 調用BookViewHolder中業務方自己的局部刷新View的方法
viewHolder.bindData(book.name, viewHolder.adapterPosition)
}
}
// 對外暴露
fun updateData(items: List<Books>) {
this.items = items
diffCallback.update(items)
// 第二個參數false是啥意思呢?簡單來說到Adapter被調用了notifyItemMoved()時,不使用動畫。
DiffUtil.calculateDiff(diffCallback, false).dispatchUpdatesTo(this)
}
}
使用的時候,直接調用updateData()
,傳入我們的新數據集合,無需任何其他操作。
到這我們的DiffUtil
用法就結束了。我相信已經開始用DiffUtil
的小夥伴一定會遇到下面這個問題:
數據集合都是同一個,因爲都是直接操作同一個集合的引用,因此導致DiffUtil的時候各種不生效。
三、DiffUtil的思考
上述的問題,我猜很多小夥伴都遇到過。因爲一些模式或者架構的原因。導致我們很多邏輯操作,都是使用同一個集合的引用,因此改變也是同一個集合元素。那麼這種情況下對於DiffUtil
來說,至始至終oldData和newData都是同一個集合,那就不存在diff這一說了。
如果大家能感受到這其中的彆扭之處,那麼離理解,我想要聊的函數式編程就不遠。
因爲DiffUtil
設計本身就是對不同的集合對象進行diff。因此我們在update的時候,就必須要輸入倆個不同的集合實例。
而這恰恰滿足了函數式編程(Functional Programming)所強調的倆點中的一點:不可變(immutable)。注意這個英文單詞immutable,以及於之對立的mutable。不知道大家有沒有留意到Kotlin中,大量的使用了這類單詞。簡單舉幾個例子:
MutableList
、MutableMap
…等等
函數式編程強調的另一點是:無狀態(stateless)
大家再思考一個問題:RecycleView是啥?不就是一個UI控件麼。我們要做的是啥?不就是給RecycleView一個數據集,然後讓它展示出來。
那麼我們簡化一下RecycleView的這個模型,是不是RecycleView的這一系列操作就像一個函數/公式?給定一個輸入,必定有一個輸出。
嘚吧嘚,扯了這麼多“玄之又玄”的東西,想表達啥意思呢。UI操作本身就像函數表達式一樣,至於一切的數據變化,狀態改變,那是輸入給UI前的變換(transform)。
還記不記得咱們在上一個系列聊MVVM的時候,提到了數據驅動。Google抽象出了ViewModel
就是讓我們去做數據的變換(transform)。變換完畢之後再輸入給UI模塊。對於咱們的例子來說,在Viewmodel之中變換完數據,把變化後的新數據集合,丟給DiffUtil,這纔是正確的使用方式。
而這次整個系列所聊的內容就基本發生在ViewModel
這一層。我們應該使用函數編程的思想去transform數據集合。
尾聲
無論是面向對象,還是面向過程,亦或者函數式編程…本身都沒有特別明確的邊界。我們要做的應該是讓優秀的思想爲我們所用,提高我們的生產力,最終實現“面向自由編程”~~~
最後,與君共勉!