本文將對Paging
分頁組件的設計和實現進行一個系統總體的概述,強烈建議 讀者將本文做爲學習Paging
閱讀優先級最高的文章,全部其它的Paging
中文博客閱讀優先級都應該靠後。git
本文篇幅 較長,總體結構思惟導圖以下:github
手機應用中,列表是常見的界面構成元素,而對於Android開發者而言,RecyclerView
是實現列表的不二選擇。數據庫
在正式討論Paging
和列表分頁功能以前,咱們首先看看對於一個普通的列表,開發者如何經過代碼對其進行建模:api
如圖所示,針對這樣一個簡單 聯繫人界面 的建模,咱們引出3個重要的層級:緩存
爲何說 服務端組件、數據庫 以及 內存 是很是重要的三個層級呢?服務器
首先,開發者爲當前頁面建立了一個ViewModel
,並經過成員變量在 內存 中持有了一組聯繫人數據,由於ViewModel
組件的緣由,即便頁面配置發生了改變(好比屏幕的旋轉),數據依然會被保留下來。網絡
而 數據庫 的做用則保證了App
即便在離線環境下,用戶依然能夠看到必定的內容——顯然對於上圖中的頁面(聯繫人列表)而言,本地緩存是很是有意義的。數據結構
對於絕大多數列表而言,服務端 每每意味着是數據源,每當用戶執行刷新操做,App
都應當嘗試向服務端請求最新的數據,並將最新的數據存入 數據庫,並隨之展現在UI
上。架構
一般狀況下,這三個層級並不是同時都是必要的,讀者需正確理解三者各自不一樣的使用場景。app
如今,藉助於 服務端組件、數據庫 以及 內存,開發者將數據展現在RecyclerView
上,這彷佛已是正解了。
到目前爲止,問題尚未徹底暴露出來。
咱們忽視了一個很是現實的問題,那就是 數據是動態的 ——這意味着,每當數據發生了更新(好比用戶進行了下拉刷新操做),開發者都須要將最新的數據響應在UI
上。
這意味着,當某個用戶的聯繫人列表中有10000個條目時,每次數據的更新,都會對全部的數據進行重建——從而致使 性能很是低下,用戶看到的只是屏幕中的幾條聯繫人信息,爲此要從新建立10000個條目?用戶顯然沒法接受。
所以,分頁組件的設計勢在必行。
上文咱們談到,UI響應數據的變動,這種狀況下,使用 觀察者模式 是一個不錯的主意,好比LiveData
、RxJava
甚至自定義一個接口等等,開發者僅須要觀察每次數據庫中數據的變動,並進行UI
的更新:
class MyViewModel : ViewModel() {
val users: LiveData<List<User>>
}
複製代碼
新的組件咱們也但願能擁有一樣的便利,好比使用LiveData
或者RxJava
,並進行訂閱處理數據的更新—— 簡單 且 易用。
咱們但願新的組件可以處理多層,咱們但願列表展現 服務器 返回的數據、 或者 數據庫 中的數據,並將其放入UI中。
新的組件必須保證足夠的快,不作任何不必的行爲,爲了保證效率,繁重的操做不要直接放在UI
線程中處理。
若是可能,新的組件須要可以對生命週期進行感知,就像LiveData
同樣,若是頁面並不在屏幕的可視範圍內,組件不該該工做。
足夠的靈活性很是重要——每一個項目都有不一樣的業務,這意味着不一樣的API
、不一樣的數據結構,新的組件必須保證可以應對全部的業務場景。
這一點並不是必須,可是對於設計者來講難度不小,這意味着須要將不一樣的業務中的共同點抽象出來,並保證這些設計適用在任何場景中。
定義好了需求,在正式開始設計Paging以前,首先咱們先來回顧一下,普通的列表如何實現數據的動態更新的。
咱們依然經過 聯繫人列表 做爲示例,來描述普通列表 如何響應數據的動態更新。
首先,咱們須要定義一個Dao
,這裏咱們使用了Room
組件用於 數據庫 中聯繫人的查詢:
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun queryUsers(): LiveData<List<User>>
}
複製代碼
這裏咱們返回的是一個LiveData
,正如咱們前文所言,構建一個可觀察的對象顯然會讓數據的處理更加容易。
接下來咱們定義好ViewModel
和Activity
:
class MyViewModel(val dao: UserDao) : ViewModel() {
// 1.定義好可觀察的LiveData
val users: LiveData<List<User>> = dao.queryUsers()
}
class MyActivity : Activity {
val myViewModel: MyViewModel
val adapter: ListAdapter
fun onCreate(bundle: Bundle?) {
// 2.在Activity中對LiveData進行訂閱
myViewModel.users.observe(this) {
// 3.每當數據更新,計算新舊數據集的差別,對列表進行更新
adapter.submitList(it)
}
}
}
複製代碼
這裏咱們使用到了ListAdapter
,它是官方基於RecyclerView.Adapter
的AsyncListDiffer
封裝類,其內建立了AsyncListDiffer
的示例,以便在後臺線程中使用DiffUtil
計算新舊數據集的差別,從而節省Item
更新的性能。
本文默認讀者對
ListAdapter
必定了解,若是不是很熟悉,請參考DiffUtil
、AsyncListDiffer
、ListAdapter
等相關知識點的文章。
此外,咱們還須要在ListAdapter
中聲明DiffUtil.ItemCallback
,對數據集的差別計算的邏輯進行補充:
class MyAdapter(): ListAdapter<User, UserViewHolder>(
object: DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User)
= oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: User, newItem: User)
= oldItem == newItem
}
) {
// ...
}
複製代碼
That's all, 接下來咱們開始思考,新的分頁組件應該是什麼樣的。
上文提到,一個普通的RecyclerView
展現的是一個列表的數據,好比List<User>
,但在列表分頁的需求中,List<User>
明顯就不太夠用了。
爲此,Google
設計出了一個新的角色PagedList
,顧名思義,該角色的意義就是 分頁列表數據的容器 。
既然有了
List
,爲何須要額外設計這樣一個PagedList
的數據結構?本質緣由在於加載分頁數據的操做是異步的 ,所以定義PagedList
的第二個做用是 對分頁數據的異步加載 ,這個咱們後文再提。
如今,咱們的ViewModel
如今能夠定義成這樣,由於PagedList
也做爲列表數據的容器(就像List<User>
同樣):
class MyViewModel : ViewModel() {
// before
// val users: LiveData<List<User>> = dao.queryUsers()
// after
val users: LiveData<PagedList<User>> = dao.queryUsers()
}
複製代碼
在ViewModel
中,開發者能夠輕易經過對users
進行訂閱以響應分頁數據的更新,這個LiveData
的可觀察者是經過Room
組件建立的,咱們來看一下咱們的dao
:
@Dao
interface UserDao {
// 注意,這裏 LiveData<List<User>> 改爲了 LiveData<PagedList<User>>
@Query("SELECT * FROM user")
fun queryUsers(): LiveData<PagedList<User>>
}
複製代碼
乍得一看彷佛理所固然,但實際需求中有一個問題,這裏的定義是模糊不清的——對於分頁數據而言,不一樣的業務場景,所須要的相關配置是不一樣的。那麼什麼是分頁相關配置呢?
最直接的一點是每頁數據的加載數量PageSize
,不一樣的項目都會自行規定每頁數據量的大小,一頁請求15個數據仍是20個數據?顯然咱們目前的代碼沒法進行配置,這是不合理的。
回答這個問題以前,咱們還須要定義一個角色,用來爲PagedList
容器提供分頁數據,那就是數據源DataSource
。
什麼是DataSource
呢?它不該該是 數據庫數據 或者 服務端數據, 而應該是 數據庫數據 或者 服務端數據 的一個快照(Snapshot
)。
每當Paging
被告知須要更多數據:「Hi,我須要第45-60個的數據!」——數據源DataSource
就會將當前Snapshot
對應索引的數據交給PagedList
。
可是咱們須要構建一個新的PagedList
的時候——好比數據已經失效,DataSource
中舊的數據沒有意義了,所以DataSource
也須要被重置。
在代碼中,這意味着新的DataSource
對象被建立,所以,咱們須要提供的不是DataSource
,而是提供DataSource
的工廠。
爲何要提供
DataSource.Factory
而不是一個DataSource
? 複用這個DataSource
不能夠嗎,固然能夠,可是將DataSource
設置爲immutable
(不可變)會避免更多的未知因素。
從新整理思路,咱們如何定義Dao
中接口的返回值呢?
@Dao
interface UserDao {
// Int 表明按照數據的位置(position)獲取數據
// User 表明數據的類型
@Query("SELECT * FROM user")
fun queryUsers(): DataSource.Factory<Int, User>
}
複製代碼
返回的是一個數據源的提供者DataSource.Factory
,頁面初始化時,會經過工廠方法建立一個新的DataSource
,這以後對應會建立一個新的PagedList
,每當PagedList
想要獲取下一頁的數據,數據源都會根據請求索引進行數據的提供。
當數據失效時,DataSource.Factory
會再次建立一個新的DataSource
,其內部包含了最新的數據快照(本案例中表明着數據庫中的最新數據),隨後建立一個新的PagedList
,並從DataSource
中取最新的數據進行展現——固然,這以後的分頁流程都是相同的,無需再次複述。
筆者繪製了一幅圖用於描述三者之間的關係,讀者可參考上述文字和圖片加以理解:
迴歸第一小節的那個問題,分頁相關業務如何進行配置?咱們雖然介紹了爲PagedList
提供數據的DataSource
,但這個問題彷佛仍是沒有獲得解決。
此外,如今Dao
中接口的返回值已是DataSource.Factory
,而ViewModel
中的成員被觀察者則是LiveData<PagedList<User>>
類型,如何 將數據源的工廠和LiveData<PagedList>
進行串聯 ?
所以咱們還須要定義一個新的角色PagedListBuilder
,開發者將 數據源工廠 和 相關配置 統一交給PagedListBuilder
,便可生成對應的LiveData<PagedList<User>>
:
class MyViewModel(val dao: UserDao) : ViewModel() {
val users: LiveData<PagedList<User>>
init {
// 1.建立DataSource.Factory
val factory: DataSource.Factory = dao.queryUsers()
// 2.經過LivePagedListBuilder配置工廠和pageSize, 對users進行實例化
users = LivePagedListBuilder(factory, 30).build()
}
}
複製代碼
如代碼所示,咱們在ViewModel
中先經過dao
獲取了DataSource.Factory
,工廠建立數據源DataSource
,後者爲PagedList
提供列表所須要的數據;此外,另一個Int
類型的參數則制定了每頁數據加載的數量,這裏咱們指定每頁數據數量爲30。
咱們成功建立了一個LiveData<PagedList<User>>
的可觀察者對象,接下來的步驟讀者得心應手,只不過咱們這裏使用的是PagedListAdapter
:
class MyActivity : Activity {
val myViewModel: MyViewModel
// 1.這裏咱們使用PagedListAdapter
val adapter: PagedListAdapter
fun onCreate(bundle: Bundle?) {
// 2.在Activity中對LiveData進行訂閱
myViewModel.users.observe(this) {
// 3.每當數據更新,計算新舊數據集的差別,對列表進行更新
adapter.submitList(it)
}
}
}
複製代碼
PagedListAdapter
內部的實現和普通列表ListAdapter
的代碼幾乎徹底相同:
// 幾乎徹底相同的代碼,只有繼承的父類不一樣
class MyAdapter(): PagedListAdapter<User, UserViewHolder>(
object: DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User)
= oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: User, newItem: User)
= oldItem == newItem
}
) {
// ...
}
複製代碼
準確的來講,二者內部的實現還有微弱的區別,前者
ListAdapter
的getItem()
函數的返回值是User
,然後者PagedListAdapter
返回值應該是User?
(Nullable),其緣由咱們會在下面的Placeholder
部分進行描述。
目前的介紹中,分頁的功能彷佛已經實現完畢,但這些在現實開發中每每不夠,產品業務還有更多細節性的需求。
在上一小節中,咱們經過LivePagedListBuilder
對LiveData<PagedList<User>>
進行建立,這其中第二個參數是 分頁組件的配置,表明了每頁加載的數量(PageSize
) :
// before
val users: LiveData<PagedList<User>> = LivePagedListBuilder(factory, 30).build()
複製代碼
讀者應該理解,分頁組件的配置 自己就是抽象的,PageSize
並不能徹底表明它,所以,設計者額外定義了更復雜的數據結構PagedList.Config
,以描述更細節化的配置參數:
// after
val config = PagedList.Config.Builder()
.setPageSize(15) // 分頁加載的數量
.setInitialLoadSizeHint(30) // 初次加載的數量
.setPrefetchDistance(10) // 預取數據的距離
.setEnablePlaceholders(false) // 是否啓用佔位符
.build()
// API發生了改變
val users: LiveData<PagedList<User>> = LivePagedListBuilder(factory, config).build()
複製代碼
對複雜業務配置的API
設計來講,建造者模式 顯然是不錯的選擇。
接下來咱們簡單瞭解一下,這些可選的配置分別表明了什麼。
最易理解的配置,分頁請求數據時,開發者老是須要定義每頁加載數據的數量。
定義首次加載時要加載的Item
數量。
此值一般大於PageSize
,所以在初始化列表時,該配置可使得加載的數據保證屏幕能夠小範圍的滾動。
若是未設置,則默認爲PageSize
的三倍。
顧名思義,該參數配置定義了列表當距離加載邊緣多遠時進行分頁的請求,默認大小爲PageSize
——即距離底部還有一頁數據時,開啓下一頁的數據加載。
若該參數配置爲0,則表示除非明確要求,不然不會加載任何數據,一般不建議這樣作,由於這將致使用戶在滾動屏幕時看到佔位符或列表的末尾。
該配置項須要傳入一個boolean
值以決定列表是否開啓placeholder
(佔位符),那麼什麼是placeholder
呢?
咱們先來看未開啓佔位符的狀況:
如圖所示,沒有開啓佔位符的狀況下,列表展現的是當前全部的數據,請讀者重點觀察圖片右側的滾動條,當滾動到列表底部,成功加載下一頁數據後,滾動條會從長變短,這意味着,新的條目成功實裝到了列表中。一言以蔽之,未開啓佔位符的列表,條目的數量和PagedList
中數據數量是一致的。
接下來咱們看一下開啓了佔位符的狀況:
如圖所示,開啓了佔位符的列表,條目的數量和DataSource
中數據的總量是一致的。 這並不意味着列表從DataSource
一次加載了大量的數據並進行渲染,全部業務依然交給Paging
進行分頁處理。
當用戶滑動到了底部還沒有加載的數據時,開發者會看到還未渲染的條目,這是理所固然的,PagedList
的分頁數據加載是異步的,這時對於Item
的來講,要渲染的數據爲null
,所以開發者須要配置佔位符,當數據未加載完畢時,UI如何進行渲染——這也正是爲什麼上文說到,對於PagedListAdapter
來講,getItem()
函數的返回值是可空的User?
,而不是User
。
隨着PagedList
下一頁數據的異步加載完畢,伴隨着RecyclerView
的原生動畫,新的數據會被從新覆蓋渲染到placeholder
對應的條目上,就像gif
圖展現的同樣。
這裏我專門開一個小節談談關於placeholder
,由於這個機制和咱們傳統的分頁業務彷佛有所不一樣,但Google
的工程師們認爲在某些業務場景下,該配置確實頗有用。
開啓了佔位符,用戶老是能夠快速的滑動列表,由於列表「持有」了整個數據集,所以不會像未開啓佔位符時,滑動到底部而被迫暫停滾動,直到新的數據的加載完畢才能繼續瀏覽。順暢的操做總比指望以外的阻礙要好得多 。
此外,開啓了佔位符意味着用戶與 加載指示器 完全告別,相似一個 正在加載更多... 的提示標語或者一個簡陋的ProgressBar
效果然的會提高用戶體驗嗎?也許答案是否認的,相比之下,用戶應該更喜歡一個灰色的佔位符,並等待它被新的數據渲染。
但缺點也隨之而來,首先,佔位符的條目高度應該和正確的條目高度一致,在某些需求中,這也許並不符合,這將致使漸進性的動畫效果並不會那麼好。
其次,對於開發者而言,開啓佔位符意味着須要對ViewHolder
進行額外的代碼處理,數據爲null
或者不爲null
?兩種狀況下的條目渲染邏輯都須要被添加。
最後,這是一個限制性的條件,您的DataSource
數據源內部的數據數量必須是肯定的,好比經過Room
從本地獲取聯繫人列表;而當數據經過網絡請求獲取的話,這時數據的數量是不肯定的,不開啓Placeholder
反而更好。
在本文的示例中,咱們創建了一個LiveData<PagedList<User>>
的可觀察者對象供用戶響應數據的更新,實際上組件的設計應該面向提供對更多優秀異步庫的支持,好比RxJava
。
所以,和LivePagedListBuilder
同樣,設計者還提供了RxPagedListBuilder
,經過DataSource
數據源和PagedList.Config
以構建一個對應的Observable
:
// LiveData support
val users: LiveData<PagedList<User>> = LivePagedListBuilder(factory, config).build()
// RxJava support
val users: Observable<PagedList<User>> = RxPagedListBuilder(factory, config).buildObservable()
複製代碼
Paging
幕後是如何工做的?
接下來,筆者將針對Paging
分頁組件的工做流程進行系統性的描述,探討Paging
是 如何實現異步分頁數據的加載和響應 的。
爲了便於理解,筆者將整個流程拆分爲三個步驟,併爲每一個步驟繪製對應的一張流程圖,這三個步驟分別是:
如圖所示,咱們定義了ViewModel
和Repository
,Repository
內部實現了App
的數據加載的邏輯,而其左側的ViewModel
則負責與UI
組件的通訊。
Repository
負責爲ViewModel
中的LiveData<PagedList<User>>
進行建立,所以,開發者須要建立對應的PagedList.Config
分頁配置對象和DataSource.Factory
數據源的工廠,並經過調用LivePagedListBuilder
相關的API
建立出一個LiveData<PagedList<User>>
。
當LiveData
一旦被訂閱,Paging
將會嘗試建立一個PagedList
,同時,數據源的工廠DataSource.Factory
也會建立一個DataSource
,並交給PagedList
持有該DataSource
。
這時候PagedList
已經被成功的建立了,可是此時的PagedList
內部只持有了一個DataSource
,卻並無持有任何數據,這意味着觀察者角色的UI
層即將接收到一個空數據的PagedList
。
這沒有任何意義,所以咱們更但願PagedList
第一次傳遞到UI
層級的同時,已經持有了初始的列表數據(即InitialLoadSizeHint
);所以,Paging
嘗試在後臺線程中經過DataSource
對PagedList
內部的數據列表進行初始化。
如今,PagedList
第一次建立完畢,並持有屬於本身的DataSource
和初始的列表數據,經過LiveData
這個管道,即將向UI
層邁出屬於本身的第一個腳印。
經過內部線程的切換,PagedList
從後臺線程切換到了UI
線程,經過LiveData
抵達了UI
層級,也就是咱們一般說的Activity
或者Fragment
中。
讀者應該有印象,在上文的示例代碼中,Activity
觀察到PagedList
後,會經過PagedListAdapter.submitList()
函數將PagedList
進行注入。PagedListAdapter
第一次接收到PagedList
後,就會對UI
進行渲染。
當用戶嘗試對屏幕中的列表進行滾動時,咱們接收到了須要加載更多數據的信號,這時,PagedList
在內部主動觸發數據的加載,數據源提供了更多的數據,PagedList
接收到以後將會主動觸發RecyclerView
的更新,用戶經過RecyclerView
原生動畫觀察到了更多的列表Item
。
當數據發生了更新,Paging
幕後又作了哪些工做呢?
正如前文所說,數據是動態的, 假設用戶經過操做添加了一個聯繫人,這時數據庫中的數據集發生了更新。
所以,這時屏幕中RecyclerView
對應的PagedList
和DataSource
已經沒有失效了,由於DataSource
中的數據是以前數據庫中數據的快照,數據庫內部進行了更新,PagedList
從舊的DataSource
中再取數據毫無心義。
所以,Paging
組件接收到了數據失效的信號,這意味着生產者須要從新構建一個PagedList
,所以DataSource.Factory
再次提供新版本的數據源DataSource V2
——其內部持有了最新數據的快照。
在建立新的PagedList
的時候,針對PagedList
內部的初始化須要慎重考慮,由於初始化的數據須要根據用戶當前屏幕中所在的位置(position
)進行加載。
經過LiveData
,UI
層級再次觀察到了新的PagedList
,並再次經過submitList()
函數注入到PagedListAdapter
中。
和初次的數據渲染不一樣,這一次咱們使用到了PagedListAdapter
內部的AsyncPagedListDiffer
對兩個數據集進行差別性計算——這避免了notifyDataSetChanged()
的濫用,同時,差別性計算的任務被切換到了後臺線程中執行,一旦計算出差別性結果,新的PagedList
會替換舊的PagedList
,並對列表進行 增量更新。
Paging
分頁組件的設計中,DataSource
是一個很是重要的模塊。顧名思義,DataSource<Key, Value>
中的Key
對應數據加載的條件,Value
對應數據集的實際類型, 針對不一樣場景,Paging
的設計者提供了三種不一樣類型的DataSource
抽象類:
PositionalDataSource<T>
ItemKeyedDataSource<Key, Value>
PageKeyedDataSource<Key, Value>
接下來咱們分別對其進行簡單的介紹。
本章節涉及的知識點很是重要,但不做爲本文的重點,筆者將在該系列的下一篇文章中針對
DataSource
的設計與實現進行更細節的探究,歡迎關注。
PositionalDataSource<T>
是最簡單的DataSource
類型,顧名思義,其經過數據所處當前數據集快照的位置(position
)提供數據。
PositionalDataSource<T>
適用於 目標數據總數固定,經過特定的位置加載數據,這裏Key
是Integer
類型的位置信息,而且被內置固定在了PositionalDataSource<T>
類中,T
即數據的類型。
最容易理解的例子就是本文的聯繫人列表,其全部的數據都來自本地的數據庫,這意味着,數據的總數是固定的,咱們老是能夠根據當前條目的position
映射到DataSource
中對應的一個數據。
PositionalDataSource<T>
也正是Room
幕後實現的功能,使用Room
爲何能夠避免DataSource
的配置,經過dao
中的接口就能返回一個DataSource.Factory
?
來看Room
組件配置的dao
對應編譯期生成的源碼:
// 1.Room自動生成了 DataSource.Factory
@Override
public DataSource.Factory<Integer, Student> getAllStudent() {
// 2.工廠函數提供了PositionalDataSource
return new DataSource.Factory<Integer, Student>() {
@Override
public PositionalDataSource<Student> create() {
return new PositionalDataSource<Student>(__db, _statement, false , "Student") {
// ...
};
}
};
}
複製代碼
ItemKeyedDataSource<Key, Value>
適用於目標數據的加載依賴特定條目的信息,好比須要根據第N項的信息加載第N+1項的數據,傳參中須要傳入第N項的某些信息時。
一樣拿聯繫人列表舉例,另外的一種分頁加載方式是經過上一個聯繫人的name
做爲Key
請求新一頁的數據,由於聯繫人name
字母排序的緣由,DataSource
很容易針對一個name
檢索並提供接下來新一頁的聯繫人數據——好比根據Alice
找到下一個用戶Bob
(A -> B
)。
更多的網絡請求API
中,服務器返回的數據中都會包含一個String
類型相似nextPage
的字段,以表示當前頁數據的下一頁數據的接口(好比Github
的API
),這種分頁數據加載的方式正是PageKeyedDataSource<Key, Value>
的拿手好戲。
這是平常開發中用到最多的DataSource
類型,和ItemKeyedDataSource<Key, Value>
不一樣的是,前者的數據檢索關係是單個數據與單個數據之間的,後者則是每一頁數據和每一頁數據之間的。
一樣拿聯繫人列表舉例,這種分頁加載方式是按照頁碼進行數據加載的,好比一次請求15條數據,服務器返回數據列表的同時會返回下一頁數據的url
(或者頁碼),藉助該參數請求下一頁數據成功後,服務器又回返回下下一頁的url
,以此類推。
總的來講,DataSource
針對不一樣種數據分頁的加載策略提供了不一樣種的抽象類以方便開發者調用,不少狀況下,一樣的業務使用不一樣的DataSource
都可以實現,開發者按需取用便可。
如今讀者對多種不一樣的數據源DataSource
有了簡單的瞭解,先拋開 分頁列表 的業務不談,咱們思考另一個問題:
當列表的數據經過多個層級 網絡請求(
Network
) 和 本地緩存 (Database
)進行加載該怎麼處理?
回答這個問題,須要先思考另一個問題:
Network
+Database
的解決方案有哪些優點?
讀者認真思考可得,Network
+Database
的解決方案優勢以下:
App
的終止,本地緩存能夠對頁面數據進行快速恢復,大幅減小流量的損失,以及加載的時間。看起來Network
+Database
是一個很是不錯的數據加載方案,那麼爲何大多數場景並無使用本地緩存呢?
主要緣由是開發成本——本地緩存的搭建老是須要額外的代碼,不只如此,更重要的緣由是,數據交互的複雜性也會致使額外的開發成本。
爲何說Network
+Database
會致使 數據交互的複雜性 ?
讓咱們回到本文的 聯繫人列表 的示例中,這個示例中,全部聯繫人數據都來自 本地緩存,所以讀者能夠很輕易的構建出該功能的總體結構:
如圖所示,ViewModel
中的數據老是由Database
提供,若是把數據源從Database
換成Network
,數據交互的模型也並無什麼區別—— 數據源老是單一的。
那麼,當數據的來源不惟一時——即Network
+Database
的數據加載方案中會有哪些問題呢?
咱們來看看常規的實現方案的數據模型:
如圖所示,ViewModel
嘗試加載數據時,老是會先進行網絡判斷,若網絡未鏈接,則展現本地緩存,不然請求網絡,而且在網絡請求成功時,將數據保存本地。
乍得一看,這種方案彷佛並無什麼問題,實際上卻有兩個很是大的弊端:
首先,經過一個boolean
類型的值就能表明網絡鏈接的狀態嗎?顯而易見,答案是否認的。
實際上,在某些業務場景下,服務器的鏈接狀態能夠是更爲複雜的,好比接收到了部分的數據包?好比某些狀況下網絡請求錯誤,這時候是否須要從新展現本地緩存?
若涉及到網絡請求的重試則更復雜,成功展現網絡數據,再次失敗展現緩存——業務愈來愈複雜,咱們甚至會逐漸沉浸其中沒法自拔,最終醒悟,這種數據的交互模型徹底不夠用了 。
另一個很明顯的弊端則是,當網絡鏈接狀態良好的時候,用戶看到的數據老是服務器返回的數據。
這種狀況下,請求的數據再次存入本地緩存彷佛毫無心義,由於網絡環境的通暢,Database
中的緩存歷來未做爲數據源被展現過。
使用 單一數據源 (single source of truth
)的好處不言而喻,正如上文所闡述的,多個數據源 反而會將業務邏輯變得愈來愈複雜,所以,咱們設計出這樣的模型:
ViewModel
若是響應Database
中的數據變動,且Database
做爲惟一的數據來源?
其思路是:ViewModel
只從Database
中取得數據,當Database
中數據不夠時,則向Server
請求網絡數據,請求成功,數據存入Database
,ViewModel
觀察到Database
中數據的變動,並更新到UI
中。
這彷佛沒法知足上文中的需求?讀者認真思考可知,實際上是沒問題的,當網絡鏈接發生故障時,這時向服務端請求數據失敗,並不會更新Database
,所以UI
展現的正是指望的本地緩存。
ViewModel
僅僅響應Database
中數據的變動,這種使用 單一數據源 的方式讓複雜的業務邏輯簡化了不少。
如今咱們理解了 單一數據源 的好處,該方案在分頁組件中也一樣適用,咱們惟一須要實現的是,如何主動觸發服務端數據的請求?
這是固然的,由於Database
中依賴網絡請求成功以後的數據存儲更新,不然列表所展現的永遠是Database
中不變的數據——別忘了,ViewModel
和Server
之間並無任何關係。
針對Database
中的數據更新,簡單的方式是 直接進行網絡請求,這種方式使用很是廣泛,好比,列表須要下拉刷新,這時主動請求網絡,網絡請求成功後將數據存入數據庫便可,這時ViewModel
響應到數據庫中的更新,並將最新的數據更新在UI
上。
另一種方式則和Paging
分頁組件自己有關,當列表滾動到指定位置,須要對下一頁數據進行加載時,如何向網絡拉取最新數據?
Paging
爲此提供了BoundaryCallback
類用於配置分頁列表自動請求分頁數據的回調函數,其做用是,當數據庫中最後一項數據被加載時,則會調用其onItemAtEndLoaded
函數:
class MyBoundaryCallback(
val database : MyLocalCache
val apiService: ApiService
) : PagedList.BoundaryCallback<User>() {
override fun onItemAtEndLoaded(itemAtEnd: User) {
// 請求網絡數據,並更新到數據庫中
requestAndAppendData(apiService, database, itemAtEnd)
}
}
複製代碼
BoundaryCallback
類爲Paging
經過Network
+Database
進行分頁加載的功能完成了最後一塊拼圖,如今,分頁列表全部數據都來源於本地緩存,而且複雜的業務實現起來也足夠靈活。
經過Network
+Database
進行Paging
分頁加載還有更多好處,好比更輕易管理分頁列表 額外的狀態 。
不只僅是分頁列表,這種方案使得全部列表的 狀態管理 的更加容易,筆者爲此撰寫了另一篇文章去闡述它,篇幅所限,本文不進行展開,有興趣的讀者能夠閱讀。
本文對Paging
進行了系統性的概述,最後,Paging
究竟是一個什麼樣的分頁庫?
首先,它支持Network
、Database
或者二者,經過Paging
,你能夠輕鬆獲取分頁數據,並直接更新在RecyclerView
中。
其次,Paging
使用了很是優秀的 觀察者模式 ,其簡單的API
的內部封裝了複雜的分頁邏輯。
第三,Paging
靈活的配置和強大的支持——不一樣DataSource
的數據加載方式、不一樣的響應式庫的支持(LiveData
、RxJava
)等等,Paging
老是可以勝任分頁數據加載的需求。
再次重申,強烈建議 讀者將本文做爲學習Paging
閱讀優先級最高的文章,全部其它的Paging
中文博客閱讀優先級都應該靠後。
——是由於本文的篇幅較長嗎?(1w字的確...)不止如此,本文嘗試對Paging
的總體結構進行拆分,筆者認爲,只要對總體結構有足夠的理解,一切API
的調用都垂手可得。但若是直接上手寫代碼的話,反而容易形成 只見樹木,不見森林 之感,上手效率反而下降。
此外,本文附帶一些學習資料,供讀者參考:
本文的大綱來源於 Google I/O '18
中對Paging
的一個視頻分享,講的很是精彩,本文絕大多數內容和靈感也是由此而來,強烈建議讀者觀看。
其實也就是筆者去年寫的幾篇關於Paging
的文章:
Hello,我是 卻把清梅嗅 ,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人 博客 或者 Github。
若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?