1. 程式人生 > 其它 >recyclerview巢狀recyclerview重新整理_更高效的 RecyclerView重新整理方式,讓DiffUtil更通用!

recyclerview巢狀recyclerview重新整理_更高效的 RecyclerView重新整理方式,讓DiffUtil更通用!

技術標籤:recyclerview巢狀recyclerview重新整理

本文作者

作者:唐子玄

連結:

https://juejin.im/post/6882531923537707015

本文由作者授權釋出。

每次資料變化都全量重新整理整個列表是很奢侈的,不僅整個列表會閃爍一下,而且所有可見表項都會重新執行一遍onBindViewHolder()並重繪列表(即便它並不需要重新整理)。若表項檢視複雜,會顯著影響列表效能。

更高效的重新整理方式應該是:只重新整理資料發生變化的表項。RecyclerView.Adapter有 4 個非全量重新整理方法,分別是:notifyItemRangeInserted()、notifyItemRangeChanged()、notifyItemRangeRemoved、notifyItemMoved()。呼叫它們時都需指定變化範圍,這要求業務層瞭解資料變化的細節,無疑增加了呼叫難度。

1 DiffUtil模版程式碼

androidx.recyclerview.widget包下有一個工具類叫DiffUtil,它利用了一種演算法計算出兩個列表間差異,並且可以直接應用到RecyclerView.Adapter上,自動實現非全量重新整理。

使用DiffUtil的模版程式碼如下:

valoldList=...//老列表
valnewList=...//新列表
valadapter:RecyclerView.Adapter = ...

//1.定義比對方法
valcallback=object:DiffUtil.Callback(){
overridefungetOldListSize():Int=oldList.size
overridefungetNewListSize():Int=newList.size
overridefunareItemsTheSame(oldItemPosition:Int,newItemPosition:Int):Boolean{
//分別獲取新老列表中對應位置的元素
valoldItem=oldList[oldItemPosition]
valnewItem=newList[newItemPosition]
return...//定義什麼情況下新老元素是同一個物件(通常是業務id)
}
overridefunareContentsTheSame(oldItemPosition:Int,newItemPosition:Int):Boolean{
valoldItem=oldList[oldItemPosition]
valnewItem=newList[newItemPosition]
return...//定義什麼情況下同一物件內容是否相同(由業務邏輯決定)
}
overridefungetChangePayload(oldItemPosition:Int,newItemPosition:Int):Any?{
valoldItem=oldList[oldItemPosition]
valnewItem=newList[newItemPosition]
return...//具體定義同一物件內容是如何地不同(返回值會作為payloads傳入onBindViewHoder())
}
}
//2.進行比對並輸出結果
valdiffResult=DiffUtil.calculateDiff(callback)
//3.將比對結果應用到adapter
diffResult.dispatchUpdatesTo(adapter)

DiffUtil需要 3 個輸入,一個老列表,一個新列表,一個DiffUtil.Callback,其中的Callback的實現和業務邏輯有關,它定義瞭如何比對列表中的資料。

判定列表中資料是否相同分為遞進三個層次:

  1. 是否是同一個資料:對應areItemsTheSame()

  2. 若是同一個資料,其中具體內容是否相同:對應areContentsTheSame()(當areItemsTheSame()返回true時才會被呼叫)

  3. 若同一資料的具體內容不同,則找出不同點:對應getChangePayload()(當areContentsTheSame()返回false時才會被呼叫)

DiffUtil輸出 1 個比對結果DiffResult,該結果可以應用到RecyclerView.Adapter上:

//將比對結果應用到Adapter
publicvoiddispatchUpdatesTo(finalRecyclerView.Adapteradapter){
dispatchUpdatesTo(newAdapterListUpdateCallback(adapter));
}

//將比對結果應用到ListUpdateCallback
publicvoiddispatchUpdatesTo(@NonNullListUpdateCallbackupdateCallback){...}

//基於RecyclerView.Adapter實現的列表更新回撥
publicfinalclassAdapterListUpdateCallbackimplementsListUpdateCallback{
privatefinalRecyclerView.AdaptermAdapter;
publicAdapterListUpdateCallback(@NonNullRecyclerView.Adapteradapter){
mAdapter=adapter;
}
@Override
publicvoidonInserted(intposition,intcount){
//區間插入
mAdapter.notifyItemRangeInserted(position,count);
}
@Override
publicvoidonRemoved(intposition,intcount){
//區間移除
mAdapter.notifyItemRangeRemoved(position,count);
}
@Override
publicvoidonMoved(intfromPosition,inttoPosition){
//移動
mAdapter.notifyItemMoved(fromPosition,toPosition);
}
@Override
publicvoidonChanged(intposition,intcount,Objectpayload){
//區間更新
mAdapter.notifyItemRangeChanged(position,count,payload);
}
}

DiffUtil將比對結果以ListUpdateCallback回撥的形式反饋給業務層。插入、移除、移動、更新這四個回調錶示列表內容四種可能的變化,對於RecyclerView.Adapter來說正好對應著四個非全量更新方法。

2 DiffUtil.Callback與業務解耦

不同的業務場景,需要實現不同的DiffUtil.Callback,因為它和具體的業務資料耦合。這使得它無法和上一篇介紹的型別無關介面卡一起使用。

https://juejin.im/post/6876967151975006221

有沒有辦法可以使 DiffUtil.Callback的實現和具體業務資料解耦?

這裡的業務邏輯是“比較資料是否一致”的演算法,是不是可以把這段邏輯寫在資料類體內?

擬定了一個新介面:

interfaceDiff{
//判斷當前物件和給定物件是否是同一物件
funisSameObject(other:Any):Boolean
//判斷當前物件和給定物件是否擁有相同內容
funhasSameContent(other:Any):Boolean
//返回當前物件和給定物件的差異
fundiff(other:Any):Any
}

然後讓資料類實現該介面:

dataclassText(
vartext:String,
vartype:Int,
varid:Int
):Diff{
overridefunisSameObject(other:Any):Boolean=this.id==other.id
overridefunhasSameContent(other:Any):Boolean=this.text==other.text
overridefundiff(other:Any?):Any?{
returnwhen{
other!isText->null
this.text!=other.text->{"textchange"}
else->null
}
}
}

這樣DiffUtil.Callback的邏輯就可以和業務資料解耦:

//包含任何資料型別的列表
valnewList:List=...valoldList:List=...valcallback=object:DiffUtil.Callback(){overridefungetOldListSize():Int=oldList.sizeoverridefungetNewListSize():Int=newList.sizeoverridefunareItemsTheSame(oldItemPosition:Int,newItemPosition:Int):Boolean{//將資料強轉為DiffvaloldItem=oldList[oldItemPosition]as?DiffvalnewItem=newList[newItemPosition]as?Diffif(oldItem==null||newItem==null)returnfalsereturnoldItem.isSameObject(newItem)
}overridefunareContentsTheSame(oldItemPosition:Int,newItemPosition:Int):Boolean{valoldItem=oldList[oldItemPosition]as?DiffvalnewItem=newList[newItemPosition]as?Diff
f(oldItem==null||newItem==null)returnfalsereturnoldItem.hasSameContent(newItem)
}overridefungetChangePayload(oldItemPosition:Int,newItemPosition:Int):Any?{valoldItem=oldList[oldItemPosition]as?DiffvalnewItem=newList[newItemPosition]as?Diffif(oldItem==null||newItem==null)returnnullreturnoldItem.diff(newItem)
}
}

轉念一想,所有非空類的基類Any中就包含了這些語義:

publicopenclassAny{
//用於判斷當前物件和另一個物件是否是同一個物件
publicopenoperatorfunequals(other:Any?):Boolean
//返回當前物件雜湊值
publicopenfunhashCode():Int
}

這樣就可以簡化Diff介面:

interfaceDiff{
infixfundiff(other:Any?):Any?
}

保留字infix表示這個函式的呼叫可以使用中綴表示式,以增加程式碼可讀性(效果見下段程式碼),關於它的詳細介紹可以點選這裡。

https://juejin.im/post/6844903889536286728/

資料實體類和DiffUtil.Callback的實現也被簡化:

dataclassText(
vartext:String,
vartype:Int,
varid:Int
):Diff{
overridefunhashCode():Int=this.id
overridefundiff(other:Any?):Any?{
returnwhen{
other!isText->null
this.text!=other.text->{"textdiff"}
else->null
}
}
overridefunequals(other:Any?):Boolean{
return(otheras?Text)?.text==this.text
}
}

valcallback=object:DiffUtil.Callback(){
overridefungetOldListSize():Int=oldList.size
overridefungetNewListSize():Int=newList.size
overridefunareItemsTheSame(oldItemPosition:Int,newItemPosition:Int):Boolean{
valoldItem=oldList[oldItemPosition]
valnewItem=newList[newItemPosition]
returnoldItem.hashCode()==newItem.hashCode()
}
overridefunareContentsTheSame(oldItemPosition:Int,newItemPosition:Int):Boolean{
valoldItem=oldList[oldItemPosition]
valnewItem=newList[newItemPosition]
returnoldItem==newItem
}
overridefungetChangePayload(oldItemPosition:Int,newItemPosition:Int):Any?{
valoldItem=oldList[oldItemPosition]as?Diff
valnewItem=newList[newItemPosition]as?Diff
if(oldItem==null||newItem==null)returnnull
returnoldItemdiffnewItem//中綴表示式
}
}
3 DiffUtil.calculateDiff()非同步化

比對演算法是耗時的,將其非同步化是穩妥的。

androidx.recyclerview.widget包下已經有一個可直接使用的AsyncListDiffer:

//使用時必須指定一個具體的資料型別
publicclassAsyncListDiffer<T>{
//執行比對的後臺執行緒
ExecutormMainThreadExecutor;
//用於將比對結果拋到主執行緒
privatestaticclassMainThreadExecutorimplementsExecutor{
finalHandlermHandler=newHandler(Looper.getMainLooper());
MainThreadExecutor(){}
@Override
publicvoidexecute(@NonNullRunnablecommand){
mHandler.post(command);
}
}
//提交新列表資料
publicvoidsubmitList(@NullablefinalListnewList){
//在後臺執行比對...
}
...
}

它在後臺執行緒執行比對,並將結果拋到主執行緒。可惜的是它和型別繫結,無法和無型別介面卡一起使用。

https://juejin.im/post/6876967151975006221

無奈只能參考它的思想重新寫一個自己的:

classAsyncListDiffer(
//之所以使用listUpdateCallback,目的是讓AsyncListDiffer的適用範圍不侷限於RecyclerView.Adapter
varlistUpdateCallback:ListUpdateCallback,
//自定義協程的排程器,用於適配既有程式碼,把比對邏輯放到既有執行緒中,而不是新起一個
dispatcher:CoroutineDispatcher
):DiffUtil.Callback(),CoroutineScopebyCoroutineScope(SupervisorJob()+dispatcher){
//可裝填任何型別的新舊列表
varoldList=listOf()varnewList=listOf()//用於標記每一次提交列表privatevarmaxSubmitGeneration:Int=0//提交新列表funsubmitList(newList:List<Any>){valsubmitGeneration=++maxSubmitGenerationthis.newList=newList//快速返回:沒有需要更新的東西if(this.oldList==newList)return//快速返回:舊列表為空,全量接收新列表if(this.oldList.isEmpty()){this.oldList=newList//儲存列表最新資料的快照
oldList=newList.toList()
listUpdateCallback.onInserted(0,newList.size)return
}//啟動協程比對資料
launch{valdiffResult=DiffUtil.calculateDiff([email protected])//儲存列表最新資料的快照
oldList=newList.toList()//將比對結果拋到主執行緒並應用到ListUpdateCallback介面
withContext(Dispatchers.Main){//只保留最後一次提交的比對結果,其他的都被丟棄if(submitGeneration==maxSubmitGeneration){
diffResult.dispatchUpdatesTo(listUpdateCallback)
}
}
}
}overridefungetOldListSize():Int=oldList.sizeoverridefungetNewListSize():Int=newList.sizeoverridefunareItemsTheSame(oldItemPosition:Int,newItemPosition:Int):Boolean{valoldItem=oldList[oldItemPosition]valnewItem=newList[newItemPosition]returnoldItem.hashCode()==newItem.hashCode()
}overridefunareContentsTheSame(oldItemPosition:Int,newItemPosition:Int):Boolean{valoldItem=oldList[oldItemPosition]valnewItem=newList[newItemPosition]returnoldItem==newItem
}overridefungetChangePayload(oldItemPosition:Int,newItemPosition:Int):Any?{valoldItem=oldList[oldItemPosition]as?DiffvalnewItem=newList[newItemPosition]as?Diffif(oldItem==null||newItem==null)returnnullreturnoldItemdiffnewItem
}
}

AsyncListDiffer實現了DiffUtil.Callback和CoroutineScope介面,並且將後者的實現委託給了CoroutineScope(SupervisorJob() + dispatcher)例項,這樣做的好處是在AsyncListDiffer內部任何地方可以無障礙地啟動協程,而在外部可以通過AsyncListDiffer的例項呼叫cancel()釋放協程資源。

其中關於類委託的詳細講解可以點選

Kotlin實戰 | 2 = 12 ?泛型、類委託、過載運算子綜合應用

https://juejin.im/post/6844904094537121800

關於協程的詳細講解可以點選

Kotlin 基礎 | 為什麼要這樣用協程?

https://juejin.im/post/6844904196655808519

讓無型別介面卡持有AsyncListDiffer就大功告成了:

classVarietyAdapter(
privatevarproxyList:MutableList>=mutableListOf(),
dispatcher:CoroutineDispatcher=Dispatchers.IO//預設在IO共享執行緒池中執行比對
):RecyclerView.Adapter(){//構建資料比對器privatevaldataDiffer=AsyncListDiffer(AdapterListUpdateCallback(this),dispatcher)//業務程式碼通過為dataList賦值實現填充資料vardataList:Listset(value){//將填充資料委託給資料比對器
dataDiffer.submitList(value)
}//返回上一次比對後的資料快照get()=dataDiffer.oldListoverridefunonDetachedFromRecyclerView(recyclerView:RecyclerView){
dataDiffer.cancel()//當介面卡脫離RecyclerView時釋放協程資源
}
...
}

只列出了VarietyAdapter和AsyncListDiffer相關的部分,它的詳細講解可以點選

代理模式應用 | 每當為 RecyclerView 新增型別時就很抓狂

https://juejin.im/post/6876967151975006221

然後就可以像這樣使用:

varitemNumber=1
//構建介面卡
valvarietyAdapter=VarietyAdapter().apply{
//為列表新增兩種資料型別
addProxy(TextProxy())
addProxy(ImageProxy())
//初始資料集(包含兩種不同的資料)
dataList=listOf(
Text("item${itemNumber++}"),
Image("#00ff00"),
Text("item${itemNumber++}"),
Text("item${itemNumber++}"),
Image("#88ff00"),
Text("item${itemNumber++}")
)
//預載入(上拉列表時預載入下一屏內容)
onPreload={
//獲取老列表快照(深拷貝)
valoldList=dataList
//在老列表快照尾部新增新內容
dataList=oldList.toMutableList().apply{
addAll(
listOf(
Text("item${itemNumber++}",2),
Text("item${itemNumber++}",2),
Text("item${itemNumber++}",2),
Text("item${itemNumber++}",2),
Text("item${itemNumber++}",2),
)
)
}
}
}
//應用介面卡
recyclerView?.adapter=varietyAdapter
recyclerView?.layoutManager=LinearLayoutManager(this)

Talk is cheap, show me the code:

https://github.com/wisdomtl/VarietyAdapter


最後推薦一下我做的網站,玩Android:wanandroid.com,包含詳盡的知識體系、好用的工具,還有本公眾號文章合集,歡迎體驗和收藏!

推薦閱讀:

官方也無力迴天?“SharedPreferences 存在什麼問題?” 直面底層:這一次,徹底搞懂Android 中的Window 看我一波,程式碼優化到極致的操作!

1d9ddce38e4a0cd289879f408e6655b7.png

掃一掃關注我的公眾號

如果你想要跟大家分享你的文章,歡迎投稿~

┏(^0^)┛明天見!