天天看點

Jetpack 裡面的 Paging 和 Room

paging 主要作用就是用來加載清單資料以及加載更多也就是分頁加載的,展示還是人家 recyclerview 的事兒,隻不過 paging 可以把這個過程變得更加的高效。想想咱們以前加載更多怎麼做的,我們需要關注到 recyclerview 或者 listview 通過他們知道使用者是不是滑動到頁面底部了,是不是該加載更多了,并且還需要我們自己維護加載到第幾頁,下次該加載哪一頁,而現在呢?這一切幾乎都不需要我們開發者自己維護,全部都交給 paging 吧。

說到 paging,對它很重要的就是它的資料源,也就是 datasource,而 datasource 一般分為三種,其一是隻從網絡加載資料,其二是隻從本地資料庫加載資料,其三是當本地資料庫沒有資料的時候,先從網絡加載,然後緩存到本地資料庫,而後再從資料庫響應到視圖層。

其中估計我們最常用的就是直接從網絡加載資料,但是官方的示例是上面我寫的其三那種情況,是用 retrofit 處理網絡請求,然後用 jetpack 裡面的 room 資料庫做本地緩存,是以我們還需要了解下這種方式,我們知道 room 資料庫的 Dao 層可以直接将資料以 LiveData 包裝的形式查詢出來,但是 pagelist 需要的是 datasource,是以我們需要将 Dao 層的查詢結果傳回值改為 DataSource.Factory<Key, Value>,其中的 key 指的是我們的請求參數,例如我們分頁加載時候要加載第幾頁,value 指的是查詢出來的可以給視圖層直接使用的實體。

但是我們知道視圖層的資料是由 viewmodel 管理的,而暴露給資料層又是 LiveData,是以現在我們需要做的是将從資料庫查詢出來的 datasource 轉換為視圖層可以使用的 LiveData,這時候我們就需要用到 LivePagedListBuilder 了,這是一個對 pagelist 做配置以及提供資料源的建造者,是以它執行個體化的時候需要傳入兩個參數,一個是 datasource,另外一個是關于 pagelist 的 Config,這裡傳入的 datasource 就是我們從資料庫查詢出來的那個,而 config 需要我們自己構造,我們可以配置分頁加載時候每頁加載的條數,可以配置如果資料還沒加載完成的時候是否有占位等等,這些配置完成之後,我們直接調用 LivePagedListBuilder 的 build 方法就可以得到一個 LiveData<PagedList<Value>> 執行個體了。至此我們得到了視圖層可以使用的 livedata。

很多人可能會疑問說上面提到的 pagelist 到底是幹嘛的呢?它是 pageing 裡面的另外一個重要的元件,paging 的分頁加載功能也主要是由它實作的,不過本質上它還是一個集合 list,有了資料,我們還需要給 recyclerview 展示所需的 adapter,是以我們要把平時繼承 RecyclerView.Adapter 的改為繼承 PagedListAdapter,這是一個可以和 pagelist 配合使用的 adapter,這個 adapter 與普通的 adapter 的不同之處還在于它的構造方法還需要一個 DiffUtil.ItemCallback 這是幹啥用的呢?

顧名思義就是用來計算不同的,它可以計算出兩個 pagelist 之間的不同,然後把那些不同更新到界面上,而不是暴力的将原來的所有資料替換,将整個頁面重新整理,是以這種 diff 操作能夠減少資源的消耗。但是這個不同是怎麼定義的呢?還是需要我們業務來定義,是以我們需要實作 ItemCallback 這個抽象類裡面的 areItemsTheSame 和 areContentsTheSame 方法,前者用來定義我們用 pagelist 裡面的哪個屬性來判斷兩個 item 是否相同,而後者是直接用兩個 item 的記憶體位址來判斷是否相同。

以上就是 PagedListAdapter 與普通的 adapter 的不同之處。可是寫到這裡也隻是實作了一個普通的展示一個普通清單的功能,那分頁加載功能如何實作呢?這時候就不得不提到 PagedList.BoundaryCallback 了。它會告訴我們什麼時候是初始化資料,什麼時候要加載更多。我們主要重寫 BoundaryCallback 裡面的兩個方法,其中的 onZeroItemsLoaded 告訴我們資料庫中沒有資料,代表首次加載,當然也可能是資料庫被使用者清除情況下的首次加載,onItemAtEndLoaded 則會在資料庫中最後一條資料傳回後回調,告訴我們需要再次從網絡加載資料了。

那麼 BoundaryCallback 怎麼與 pagelist 結合起來呢?可以把它當做 LivePagedListBuilder 的一個屬性給設定進去,這樣 BoundaryCallback 就可以響應到資料庫裡面的變化,進而觸發網絡請求。到這裡可能會有人有疑惑,就是網絡請求回來了,插入了資料庫,然後我們怎麼通知界面資料有變化了呢?如果按照咱們不使用 jetpack 元件的邏輯來說,可能會發個通知,讓界面再查詢一遍資料庫,或者加個接口回調之類的,但是有了 jetpack 裡面的 room,這些都不用了,因為咱們上面說了 LivePagedListBuilder 最終會構造出來一個 LiveData 給界面使用,而在每次我們插入資料的時候,這個給這個 LiveData 添加的 observer 就會響應,本質上算觀察者模式,這樣界面就能夠響應到資料庫中資料的變化。

至此我們最開始提到的第三種資料庫 + 網絡請求的模式就算說完了。不過我們平時需求中這種模式使用的并不多,更多的還是直接從網絡請求回來資料,這也就意味着我們的 datasource 變了,我們的 datasource 沒法直接由資料庫給我們提供了,我們需要自己構造 datasource。自定義 datasource,我們可以繼承 PageKeyedDataSource、ItemKeyedDataSource 以及 PositionalDataSource。這三種有什麼差別呢?PageKeyedDataSource 顧名思義就是以 page 作為 key 的 datasource 例如我們平時的上拉加載更多分頁加載就适合使用它,因為我們知道 pageId 或者要加載哪一頁就好了。而 ItemKeyedDataSource 意思則是每一下次的請求總會依賴上一次的請求結果,不像 PageKeyedDataSource 每次加載之間都那麼獨立。而第三種 PositionalDataSource 意為我們可以從任意位置處加載資料,還沒有想到應用場景。這三種中小胖覺得也就 PageKeyedDataSource 會用的比較多。

接下來我們就以最常用的 PageKeyedDataSource 來構造 datasource,來跟大家示範如何直接從網絡加載資料實作分頁加載,我們繼承了 PageKeyedDataSource 之後主要重寫兩個方法,loadInitial 這個方法調用的時候代表是首次從網絡請求資料,類似于 BoundaryCallback 裡面的 onZeroItemsLoaded,關鍵的地方在當網絡請求成功的時候我們需要将資料響應到界面上,這裡要做的操作就是使用 loadInitial 裡面的 LoadInitialCallback 的 onResult 方法進行回調,回調了之後就會将資料同步到 pagelist。loadAfter 方法調用的時候代表上滑加載更多被觸發了,需要加載更多資料了,從網絡請求完資料之後,會通過 LoadCallback 的 onResult 進行回調,進而将資料同步到 pagelist。

經過以上步驟我們隻是做了一個 datasource 的實作,具體的 DataSource 的構造還得通過繼承 DataSource.Factory 然後在其 create 方法裡面進行執行個體化,而後再将這個 datasource 傳入 LivePagedListBuilder 即可,至此之後就跟上面講的第三種模式一樣了,adapter 啥的也不用動。但是這裡實作的單純從網絡加載資料的功能隻是一個正常功能,好比那種沒有資料的情況怎麼處理?沒有更多資料的怎麼處理?這些依然需要開發者自己來處理,當在 loadInitial 裡面發出的網絡請求傳回的資料為空,就代表沒有資料,當在 loadAfter 裡面發出的網絡請求傳回的資料為空,則代表沒有更多資料。開發者可以将這些事件通過 LiveData 這種形式響應到視圖層。

一些需要特殊注意的點:

1、我們知道無論 datasource 是資料庫 + 網絡請求還是隻有網絡請求,我們最終都會通過 LivePagedListBuilder 構造出一個 LiveData 給到視圖層,但是在這兩種模式下添加在 LiveData 上的 observer 響應的頻率卻是不同的,直接 datasource 是資料庫的那種,隻要資料庫中的資料有變化,那麼 LiveData 對應的 observer 就會收到響應,也就是說每次界面要更新都必須通過此 LiveData 的 observer 操作 adapter,具體操作就是 adapter.submitList(),裡面傳入的是 pagelist。

而直接 datasource 是網絡請求跟這個是不一樣的,它隻會在那個 LiveData 首次被添加 observer 也就是變為 active 也就是 onActive 的時候會被響應一次,在這裡将 LiveData 攜帶過來的 pagelist 與 adapter 綁定,而後資料有任何變化都不會響應到 這個 LiveData 的 observer 了,那資料有變化了,怎麼響應到界面上呢?

這就得提到咱上面說的 loadInitial 和 loadAfter 了,我們上面說了在這倆方法網絡請求成功之後會調用對應的 callback 的 onResult 方法,通過打斷點會看到最終會調用到 pagelist 所在的 adapter 的 notify 方法,是以也不需要我們自己在 observer 裡面 submitList 了,換言之從資料的擷取到資料響應到 UI 上,都由 pagelist 來完成。

2、我們在 loadInitial 裡面調用 callback 的時候可以選擇是否傳入 totalCount,就是利用 totalCount 告訴 pagelist 所加載清單的總條目數,那麼告訴 pagelist 總條目數有什麼用呢?這需要結合 PagedList.Config 的 mEnablePlaceholders 屬性來看,當我們 callback 的時候傳入了 totalCount,且 setEnablePlaceholders(true),那麼我們進入清單頁面的時候,給我們的感覺就是所有的資料已經加載了,如果 recyclerview 的 scrollbar 展示的話,會發現 scrollbar 比較短,但其實此時也隻是加載了一次資料而已,之是以會有加載了所有資料的感覺,是因為我們使用了占位符,當我們快速滑動頁面的時候會看到那些還未加載的條目上面都是我們設定的預設值,隻是當資料加載完成的時候預設值會被替換為實際的值。設定占位的目的也是為了減少頁面加載資料時候的跳動感。

3、這個嚴格來說是關于 LiveData 的,咱們上面說過第三種模式也就是資料庫 + 網絡請求的,假設有這麼個需求如果一旦資料庫中已經有了緩存,就不再從網絡請求加載了,這個該怎麼實作呢?我們知道目前情況下我們從 room 資料庫裡面查詢出來的是 LiveData,可能很多人也跟我一樣第一想法就是直接拿這個 LiveData 來做判斷,因為我們可以通過 livedata.getValue 來擷取存儲在其裡面的值,這裡來說就是從資料庫中查詢出來的 list,隻要判斷這個 list 為 null 或者長度為 0 不就好了嗎?想法是完美的,可是現實是殘酷的,對于 LiveData 是不能用直接擷取值的方式來做任何判斷的,因為 LiveData 的指派是異步的,也就是說我們隻能夠在對 LiveData 添加的 observer 裡面擷取其值,也隻有在這地方才能夠擷取到它真正的值,通過我們一開說的那種直接擷取值的方式一定是 null,即使已經指派完成。

那我們知道了隻能在 observer 裡面來做判斷了,可是這不是又把資料加載的邏輯放到了視圖層嗎?因為我們隻在 activity 裡面有對這個 LiveData 添加 observer,那怎麼辦?我們也不能在資料擷取層給 LiveData 添加 observer,我們隻能在資料庫的 Dao 層在寫一條 sql,可以直接把資料庫中符合條件的資料的 count 查出來,然後進行判斷就好了。

但是還有個注意點就是關于 room 資料的操作必須都得在非 UI 線程,換言之就是隻要從資料庫中查詢出來的不是 LiveData,那麼就需要我們自己切換線程。

繼續閱讀