RecyclerView配合DiffUtil,好用到飛
一、前言
DIffUtils 是 Support-v7:24:2.0 中,更新的工具類。因為已經(jīng)更新了一段時間了,也不好說是***更新的。
它主要是為了配合 RecyclerView 使用,通過比對新、舊兩個數(shù)據(jù)集的差異,生成舊數(shù)據(jù)到新數(shù)據(jù)的最小變動,然后對有變動的數(shù)據(jù)項,進行局部刷新。
接下來就 DiffUtil 的使用細節(jié),進行一個詳細的講解,希望一篇文章就完全理解 DiffUtil。
二、為什么會有DiffUtil
RecyclerView 自從被發(fā)布以來,一直被說成是 ListView、GridView 等一系列列表控件的***替代品。并且它本身使用起來也非常的好用,布局切換方便、自帶 ViewHolder 、局部更新并且可帶更新動畫等等。
局部更新、并且可以很方便的設置更新動畫這一點,是 RecyclerView 一個不錯的亮點。它為此提供了對應的方法:
- adapter.notifyItemChange()
- adapter.notifyItemInserted()
- adapter.notifyItemRemoved()
- adapter.notifyItemMoved()
以上方法都是為了對數(shù)據(jù)集中,單一項進行操作,并且為了操作連續(xù)的數(shù)據(jù)集的變動,還提供了對應的 notifyRangeXxx() 方法。
雖然 RecyclerView 提供的局部更新的方法,看似非常的好用,但是實際上,其實并沒有什么用。
在實際開發(fā)中,最方便的做法就是無腦調(diào)用 notifyDataSetChanged(),用于更新 Adapter 的數(shù)據(jù)集。
雖然 notifyDataSetChanged() 有一些缺點:
- 不會觸發(fā) RecyclerView 的局部更新的動畫。
- 性能低,會刷新整個 RecyclerView 可視區(qū)域。
但是真有需要頻繁刷新,前后兩個數(shù)據(jù)集的場景。
方案一:使用一個 notifyDataSetChanged() 方法。
方案二:自己寫一個數(shù)據(jù)集比對方法,然后去計算他們的差值,***調(diào)用對應的方法更新到 RecyclerView 中去。
我這么懶,如果不是必要,當然是會選 方案一 了。畢竟和之前 ListView 的時候,也沒有更差了。
Google 顯然也發(fā)現(xiàn)了這個問題,所以 DiffUtil 被發(fā)布了。
三、介紹DiffUtil
就像前面說的,DiffUtil 就是為了解決這個痛點的。它能很方便的對兩個數(shù)據(jù)集之間進行比對,然后計算出變動情況,配合 RecyclerView.Adapter ,可以自動根據(jù)變動情況,調(diào)用 Adapter 的對應方法。
當然,DiffUtil 不僅只能配合 RecyclerView 使用,它實際上可以單獨用于比對兩個數(shù)據(jù)集,然后如何操作是可以定制的,那么在什么場景下使用,就全憑我們自己發(fā)揮了。
DiffUtil 在使用起來,主要需要關注幾個類:
- DiffUtil.Callback:具體用于限定數(shù)據(jù)集比對規(guī)則。
- DiffUtil.DiffResult:比對數(shù)據(jù)集之后,返回的差異結果。
1、DiffUtil.Callback
DiffUtil.Callback 主要就是為了限定兩個數(shù)據(jù)集中,子項的比對規(guī)則。畢竟開發(fā)者面對的數(shù)據(jù)結構多種多樣,既然沒法做一套通用的內(nèi)容比對方式,那么就將比對的規(guī)則,交還給開發(fā)者來實現(xiàn)即可。
在 Callback 中,其實只需要實現(xiàn) 4 個方法:
- getOldListSize():舊數(shù)據(jù)集的長度。
- getNewListSize():新數(shù)據(jù)集的長度
- areItemsTheSame():判斷是否是同一個Item。
- areContentsTheSame():如果是通一個Item,此方法用于判斷是否同一個 Item 的內(nèi)容也相同。
前兩個是獲取數(shù)據(jù)集長度的方法,這沒什么好說的。但是后兩個方法,主要是為了對應多布局的情況產(chǎn)生的,也就是存在多個 viewType 和多個 ViewHodler 的情況。首先需要使用 areItemsTheSame() 方法比對是否來自同一個 viewType(也就是同一個 ViewHolder ) ,然后再通過 areContentsTheSame() 方法比對其內(nèi)容是否也相等。
其實 Callback 還有一個 getChangePayload() 的方法,它可以在 ViewType 相同,但是內(nèi)容不相同的時候,用 payLoad 記錄需要在這個 ViewHolder 中,具體需要更新的View。
areItemsTheSame()、areContentsTheSame()、getChangePayload() 分別代表了不同量級的刷新。
首先會通過 areItemsTheSame() 判斷當前 position 下,ViewType 是否一致,如果不一致就表明當前 position 下,從數(shù)據(jù)到 UI 結構上全部變化了,那么就不關心內(nèi)容,直接更新就好了。如果一致的話,那么其實 View 是可以復用的,就還需要再通過 areContentsTheSame() 方法判斷其內(nèi)容是否一致,如果一致,則表示是同一條數(shù)據(jù),不需要做額外的操作。但是一旦不一致,則還會調(diào)用 getChangePayload() 來標記到底是哪個地方的不一樣,最終標記需要更新的地方,最終返回給 DiffResult 。
當然,對性能要是要求沒那么高的情況下,是可以不使用 getChangedPayload() 方法的。
2、DiffUtil.DiffResult
DiffUtil.DiffResult 其實就是 DiffUtil 通過 DiffUtil.Callback 計算出來,兩個數(shù)據(jù)集的差異。它是可以直接使用在 RecyclerView 上的。如果有必要,也是可以通過實現(xiàn) ListUpdateCallback 接口,來比對這些差異的。
3、使用DiffUtil
介紹了 Callback 和 DiffResult 之后,其實就可以正常使用 DiffUtil 來進行數(shù)據(jù)集的比對了。
在這個過程中,其實真的很簡單,只需要調(diào)用兩個方法:
- DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(oldDatas, newDatas), true);
- diffResult.dispatchUpdatesTo(mAdapter);
calculateDiff 方法主要是用于通過一個具體的 DiffUtils.Callback 實現(xiàn)對象,來計算出兩個數(shù)據(jù)集差異的結果,得到 DiffUtil.DiffResult 。
而 calculateDiff 的另外一個參數(shù),用于標記是否需要檢測 Item 的移動。
DiffUtil 使用的是 Eugene Myers 的差別算法,這個算法本身是不檢查元素的移動的。也就是說,有元素的移動它也只是會先標記為刪除,然后再標記插入。而如果需要計算元素的移動,它實際上也是在通過 Eugene Myers 算法比對之后,再進行一次移動檢查。所以,如果集合本身已經(jīng)排序過了,可以不進行移動的檢查。
而 dispatchUpdatesTo() 就是將這個數(shù)據(jù)集差異的結果,通過 Adapter 更新到 RecyclerView 上面。
實際上 dispatchUpdatesTo(Adapter) ,也是使用的 ListUpdateCallback 這個接口,在其中獲得差異,然后調(diào)用 Adapter 的對應方法。
四、上例子
既然已經(jīng)說清楚了,那么我們開始上例子了。
功能很簡單,有四個數(shù)據(jù)集,使用 RecyclerView 承載,然后有一個按鈕,用于輪換的切換數(shù)據(jù)集。
1、實現(xiàn) DiffUtil.Callback
為了簡單,RecyclerView 中使用單一 ViewType ,并且使用一個 TextView 承載一個 字符串來顯示。
那么我們開始實現(xiàn) Callback:
2、切換數(shù)據(jù)集
既然已經(jīng)有了 DiffUtil.Callback 的實現(xiàn)之后,我們就需要對切換數(shù)據(jù)集的點擊事件進行處理了。
3、實現(xiàn)效果
關鍵代碼已經(jīng)貼出來了,其實非常的簡單,最終運行的效果如下:
五、DiffUtil 效率問題
既然 DiffUtil 非常的好用,并且內(nèi)部也實現(xiàn)了一套算法,但是我們也需要關心它的效率問題。
根據(jù) Google 官方文檔中給出的例子,在 Nexus 5X M 系統(tǒng)上,DiffUtil 的效率問題,給出了一些參考的數(shù)據(jù):
可以看到,實際上,DiffUtil 的算法把效率問題解決的非常的好。在開啟計算移動的情況下,1000 條數(shù)據(jù)中有 200 個修改,平均值也只有 13.54 ms ,基本上都是毫秒級的。
Google 官方同時也指出,如果是對大數(shù)據(jù)集的比對,***是方在子線程中去完成計算,也就是其實是存在堵塞 UI 的情況的。所以如果你遇見了使用 DiffUtil 之后,每次刷新有卡頓的情況,可以考慮是否數(shù)據(jù)集太大,是否應該在子線程中完成計算。
六、結語
DiffUtil 已經(jīng)介紹完了,如果覺得本文對你有幫助。都看到這里了,點個贊再走吧。
【本文為51CTO專欄作者“張旸”的原創(chuàng)稿件,轉載請通過微信公眾號聯(lián)系作者獲取授權】