android实现箭头流程列表_反思|Android 列表分页组件Paging的设计与实现:系统概述...
作者:卻把清梅嗅
鏈接:https://github.com/qingmei2/blogs/issues/30
前言
本文將對Paging分頁組件的設計和實現進行一個系統整體的概述,強烈建議 讀者將本文作為學習Paging 閱讀優先級最高的文章,所有其它的Paging中文博客閱讀優先級都應該靠后。
本文篇幅 較長,整體結構思維導圖如下:
一、起源
手機應用中,列表是常見的界面構成元素,而對于Android開發者而言,RecyclerView是實現列表的不二選擇。
在正式討論Paging和列表分頁功能之前,我們首先看看對于一個普通的列表,開發者如何通過代碼對其進行建模:
如圖所示,針對這樣一個簡單 聯系人界面 的建模,我們引出3個重要的層級:
1.服務端組件、數據庫、內存
為什么說?服務端組件、數據庫?以及 內存 是非常重要的三個層級呢?
首先,開發者為當前頁面創建了一個ViewModel,并通過成員變量在 內存 中持有了一組聯系人數據,因為ViewModel組件的原因,即使頁面配置發生了改變(比如屏幕的旋轉),數據依然會被保留下來。
而?數據庫?的作用則保證了App即使在離線環境下,用戶依然可以看到一定的內容——顯然對于上圖中的頁面(聯系人列表)而言,本地緩存是非常有意義的。
對于絕大多數列表而言,服務端?往往意味著是數據源,每當用戶執行刷新操作,App都應當嘗試向服務端請求最新的數據,并將最新的數據存入?數據庫,并隨之展示在UI上。
通常情況下,這三個層級并非同時都是必要的,讀者需正確理解三者各自不同的使用場景。
現在,借助于?服務端組件、數據庫?以及 內存,開發者將數據展示在RecyclerView上,這似乎已經是正解了。
2.問題在哪?
到目前為止,問題還沒有完全暴露出來。
我們忽視了一個非常現實的問題,那就是?數據是動態的?——這意味著,每當數據發生了更新(比如用戶進行了下拉刷新操作),開發者都需要將最新的數據響應在UI上。
這意味著,當某個用戶的聯系人列表中有10000個條目時,每次數據的更新,都會對所有的數據進行重建——從而導致?性能非常低下,用戶看到的只是屏幕中的幾條聯系人信息,為此要重新創建10000個條目?用戶顯然無法接受。
因此,分頁組件的設計勢在必行。
3.整理需求
3.1、簡單易用
上文我們談到,UI響應數據的變更,這種情況下,使用?觀察者模式?是一個不錯的主意,比如LiveData、RxJava甚至自定義一個接口等等,開發者僅需要觀察每次數據庫中數據的變更,并進行UI的更新:
class MyViewModel : ViewModel() {val users: LiveData>>
}
新的組件我們也希望能擁有同樣的便利,比如使用LiveData或者RxJava,并進行訂閱處理數據的更新——?簡單?且?易用。
3.2、處理更多層級
我們希望新的組件能夠處理多層,我們希望列表展示?服務器?返回的數據、 或者?數據庫?中的數據,并將其放入UI中。
3.3、性能
新的組件必須保證足夠的快,不做任何沒必要的行為,為了保證效率,繁重的操作不要直接放在UI線程中處理。
3.4、感知生命周期 如果可能,新的組件需要能夠對生命周期進行感知,就像LiveData一樣,如果頁面并不在屏幕的可視范圍內,組件不應該工作。
3.5、足夠靈活
足夠的靈活性非常重要——每個項目都有不同的業務,這意味著不同的API、不同的數據結構,新的組件必須保證能夠應對所有的業務場景。
這一點并非必須,但是對于設計者來說難度不小,這意味著需要將不同的業務中的共同點抽象出來,并保證這些設計適用在任何場景中。
定義好了需求,在正式開始設計Paging之前,首先我們先來回顧一下,普通的列表如何實現數據的動態更新的。
4.普通列表的實現方式
我們依然通過?聯系人列表?作為示例,來描述普通列表如何響應數據的動態更新。
首先,我們需要定義一個Dao,這里我們使用了Room組件用于 數據庫 中聯系人的查詢:
@Daointerface UserDao {
@Query("SELECT * FROM user")
fun queryUsers(): LiveData>
}
這里我們返回的是一個LiveData,正如我們前文所言,構建一個可觀察的對象顯然會讓數據的處理更加容易。
接下來我們定義好ViewModel和Activity:
class MyViewModel(val dao: UserDao) : ViewModel() {// 1.定義好可觀察的LiveData
val users: LiveData> = dao.queryUsers()
}class MyActivity : Activity {val myViewModel: MyViewModelval adapter: ListAdapterfun 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.idoverride fun areContentsTheSame(oldItem: User, newItem: User)= oldItem == newItem
}
) {
// ...
}
That's all, 接下來我們開始思考,新的分頁組件應該是什么樣的。
二、分頁組件簡介
1.核心類:PagedList
上文提到,一個普通的RecyclerView展示的是一個列表的數據,比如List,但在列表分頁的需求中,List明顯就不太夠用了。
為此,Google設計出了一個新的角色PagedList,顧名思義,該角色的意義就是?分頁列表數據的容器?。
既然有了List,為什么需要額外設計這樣一個PagedList的數據結構?本質原因在于加載分頁數據的操作是異步的 ,因此定義PagedList的第二個作用是 對分頁數據的異步加載 ,這個我們后文再提。
現在,我們的ViewModel現在可以定義成這樣,因為PagedList也作為列表數據的容器(就像List一樣):
class MyViewModel : ViewModel() {// before
// val users: LiveData> = dao.queryUsers()
// after
val users: LiveData> = dao.queryUsers()
}
在ViewModel中,開發者可以輕易通過對users進行訂閱以響應分頁數據的更新,這個LiveData的可觀察者是通過Room組件創建的,我們來看一下我們的dao:
@Daointerface UserDao {
// 注意,這里 LiveData> 改成了 LiveData>
@Query("SELECT * FROM user")
fun queryUsers(): LiveData>
}
乍得一看似乎理所當然,但實際需求中有一個問題,這里的定義是模糊不清的——對于分頁數據而言,不同的業務場景,所需要的相關配置是不同的。那么什么是分頁相關配置呢?
最直接的一點是每頁數據的加載數量PageSize,不同的項目都會自行規定每頁數據量的大小,一頁請求15個數據還是20個數據?顯然我們目前的代碼無法進行配置,這是不合理的。
2.數據源: DataSource及其工廠
回答這個問題之前,我們還需要定義一個角色,用來為PagedList容器提供分頁數據,那就是數據源DataSource。
什么是DataSource呢?它不應該是?數據庫數據?或者?服務端數據, 而應該是?數據庫數據?或者?服務端數據?的一個快照(Snapshot)。
每當Paging被告知需要更多數據:“Hi,我需要第45-60個的數據!”——數據源DataSource就會將當前Snapshot對應索引的數據交給PagedList。
但是我們需要構建一個新的PagedList的時候——比如數據已經失效,DataSource中舊的數據沒有意義了,因此DataSource也需要被重置。
在代碼中,這意味著新的DataSource對象被創建,因此,我們需要提供的不是DataSource,而是提供DataSource的工廠。
為什么要提供DataSource.Factory而不是一個DataSource? 復用這個DataSource不可以嗎,當然可以,但是將DataSource設置為immutable(不可變)會避免更多的未知因素。
重新整理思路,我們如何定義Dao中接口的返回值呢?
@Daointerface UserDao {
// Int 代表按照數據的位置(position)獲取數據
// User 代表數據的類型
@Query("SELECT * FROM user")
fun queryUsers(): DataSource.Factory<Int, User>
}
返回的是一個數據源的提供者DataSource.Factory,頁面初始化時,會通過工廠方法創建一個新的DataSource,這之后對應會創建一個新的PagedList,每當PagedList想要獲取下一頁的數據,數據源都會根據請求索引進行數據的提供。
當數據失效時,DataSource.Factory會再次創建一個新的DataSource,其內部包含了最新的數據快照(本案例中代表著數據庫中的最新數據),隨后創建一個新的PagedList,并從DataSource中取最新的數據進行展示——當然,這之后的分頁流程都是相同的,無需再次復述。
筆者繪制了一幅圖用于描述三者之間的關系,讀者可參考上述文字和圖片加以理解:
3.串聯兩者:PagedListBuilder
回歸第一小節的那個問題,分頁相關業務如何進行配置?我們雖然介紹了為PagedList提供數據的DataSource,但這個問題似乎還是沒有得到解決。
此外,現在Dao中接口的返回值已經是DataSource.Factory,而ViewModel中的成員被觀察者則是LiveData>類型,如何 將數據源的工廠和LiveData進行串聯 ?
因此我們還需要定義一個新的角色PagedListBuilder,開發者將?數據源工廠?和?相關配置?統一交給PagedListBuilder,即可生成對應的LiveData>:
class MyViewModel(val dao: UserDao) : ViewModel() {val users: LiveData>init {// 1.創建DataSource.Factoryval factory: DataSource.Factory = dao.queryUsers()// 2.通過LivePagedListBuilder配置工廠和pageSize, 對users進行實例化
users = LivePagedListBuilder(factory, 30).build()
}
}
如代碼所示,我們在ViewModel中先通過dao獲取了DataSource.Factory,工廠創建數據源DataSource,后者為PagedList提供列表所需要的數據;此外,另外一個Int類型的參數則制定了每頁數據加載的數量,這里我們指定每頁數據數量為30。
我們成功創建了一個LiveData>的可觀察者對象,接下來的步驟讀者駕輕就熟,只不過我們這里使用的是PagedListAdapter:
class MyActivity : Activity {val myViewModel: MyViewModel
// 1.這里我們使用PagedListAdapter
val adapter: PagedListAdapterfun 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.idoverride fun areContentsTheSame(oldItem: User, newItem: User)= oldItem == newItem
}
) {
// ...
}
準確的來說,兩者內部的實現還有微弱的區別,前者ListAdapter的getItem()函數的返回值是User,而后者PagedListAdapter返回值應該是User?(Nullable),其原因我們會在下面的Placeholder部分進行描述。
4.更多可選配置:PagedList.Config
目前的介紹中,分頁的功能似乎已經實現完畢,但這些在現實開發中往往不夠,產品業務還有更多細節性的需求。
在上一小節中,我們通過LivePagedListBuilder對LiveData>進行創建,這其中第二個參數是 分頁組件的配置,代表了每頁加載的數量(PageSize) :
// beforeval users: LiveData> = LivePagedListBuilder(factory, 30).build()
讀者應該理解,分頁組件的配置 本身就是抽象的,PageSize并不能完全代表它,因此,設計者額外定義了更復雜的數據結構PagedList.Config,以描述更細節化的配置參數:
// afterval config = PagedList.Config.Builder()
.setPageSize(15) // 分頁加載的數量
.setInitialLoadSizeHint(30) // 初次加載的數量
.setPrefetchDistance(10) // 預取數據的距離
.setEnablePlaceholders(false) // 是否啟用占位符
.build()
// API發生了改變
val users: LiveData<PagedList<User>> = LivePagedListBuilder(factory, config).build()
對復雜業務配置的API設計來說,建造者模式 顯然是不錯的選擇。
接下來我們簡單了解一下,這些可選的配置分別代表了什么。
4.1.分頁數量:PageSize
最易理解的配置,分頁請求數據時,開發者總是需要定義每頁加載數據的數量。
4.2.初始加載數量:InitialLoadSizeHint
定義首次加載時要加載的Item數量。
此值通常大于PageSize,因此在初始化列表時,該配置可以使得加載的數據保證屏幕可以小范圍的滾動。
如果未設置,則默認為PageSize的三倍。
4.3.預取距離:PrefetchDistance
顧名思義,該參數配置定義了列表當距離加載邊緣多遠時進行分頁的請求,默認大小為PageSize——即距離底部還有一頁數據時,開啟下一頁的數據加載。
若該參數配置為0,則表示除非明確要求,否則不會加載任何數據,通常不建議這樣做,因為這將導致用戶在滾動屏幕時看到占位符或列表的末尾。
4.4.是否啟用占位符:PlaceholderEnabled
該配置項需要傳入一個boolean值以決定列表是否開啟placeholder(占位符),那么什么是placeholder呢?
我們先來看未開啟占位符的情況:
如圖所示,沒有開啟占位符的情況下,列表展示的是當前所有的數據,請讀者重點觀察圖片右側的滾動條,當滾動到列表底部,成功加載下一頁數據后,滾動條會從長變短,這意味著,新的條目成功實裝到了列表中。一言以蔽之,未開啟占位符的列表,條目的數量和PagedList中數據數量是一致的。
接下來我們看一下開啟了占位符的情況:
如圖所示,開啟了占位符的列表,條目的數量和DataSource中數據的總量是一致的。這并不意味著列表從DataSource一次加載了大量的數據并進行渲染,所有業務依然交給Paging進行分頁處理。
當用戶滑動到了底部尚未加載的數據時,開發者會看到還未渲染的條目,這是理所當然的,PagedList的分頁數據加載是異步的,這時對于Item的來說,要渲染的數據為null,因此開發者需要配置占位符,當數據未加載完畢時,UI如何進行渲染——這也正是為何上文說到,對于PagedListAdapter來說,getItem()函數的返回值是可空的User?,而不是User。
隨著PagedList下一頁數據的異步加載完畢,伴隨著RecyclerView的原生動畫,新的數據會被重新覆蓋渲染到placeholder對應的條目上,就像gif圖展示的一樣。
4.5.關于Placeholder
這里我專門開一個小節談談關于placeholder,因為這個機制和我們傳統的分頁業務似乎有所不同,但Google的工程師們認為在某些業務場景下,該配置確實很有用。
開啟了占位符,用戶總是可以快速的滑動列表,因為列表“持有”了整個數據集,因此不會像未開啟占位符時,滑動到底部而被迫暫停滾動,直到新的數據的加載完畢才能繼續瀏覽。順暢的操作總比期望之外的阻礙要好得多 。
此外,開啟了占位符意味著用戶與 加載指示器 徹底告別,類似一個 正在加載更多... 的提示標語或者一個簡陋的ProgressBar效果真的會提升用戶體驗嗎?也許答案是否定的,相比之下,用戶應該更喜歡一個灰色的占位符,并等待它被新的數據渲染。
但缺點也隨之而來,首先,占位符的條目高度應該和正確的條目高度一致,在某些需求中,這也許并不符合,這將導致漸進性的動畫效果并不會那么好。
其次,對于開發者而言,開啟占位符意味著需要對ViewHolder進行額外的代碼處理,數據為null或者不為null?兩種情況下的條目渲染邏輯都需要被添加。
最后,這是一個限制性的條件,您的DataSource數據源內部的數據數量必須是確定的,比如通過Room從本地獲取聯系人列表;而當數據通過網絡請求獲取的話,這時數據的數量是不確定的,不開啟Placeholder反而更好。
5.更多觀察者類型的配置
在本文的示例中,我們建立了一個LiveData>的可觀察者對象供用戶響應數據的更新,實際上組件的設計應該面向提供對更多優秀異步庫的支持,比如RxJava。
因此,和LivePagedListBuilder一樣,設計者還提供了RxPagedListBuilder,通過DataSource數據源和PagedList.Config以構建一個對應的Observable:
// LiveData supportval users: LiveData> = LivePagedListBuilder(factory, config).build()// RxJava support
val users: Observable> = RxPagedListBuilder(factory, config).buildObservable()
三、工作流程原理概述
Paging幕后是如何工作的?
接下來,筆者將針對Paging分頁組件的工作流程進行系統性的描述,探討Paging是 如何實現異步分頁數據的加載和響應 的。
為了便于理解,筆者將整個流程拆分為三個步驟,并為每個步驟繪制對應的一張流程圖,這三個步驟分別是:
1、初次創建流程
2、UI渲染和分頁加載流程
3、刷新數據源流程
1、初次創建流程
如圖所示,我們定義了ViewModel和Repository,Repository內部實現了App的數據加載的邏輯,而其左側的ViewModel則負責與UI組件的通信。
Repository負責為ViewModel中的LiveData>進行創建,因此,開發者需要創建對應的PagedList.Config分頁配置對象和DataSource.Factory數據源的工廠,并通過調用LivePagedListBuilder相關的API創建出一個LiveData>。
當LiveData一旦被訂閱,Paging將會嘗試創建一個PagedList,同時,數據源的工廠DataSource.Factory也會創建一個DataSource,并交給PagedList持有該DataSource。
這時候PagedList已經被成功的創建了,但是此時的PagedList內部只持有了一個DataSource,卻并沒有持有任何數據,這意味著觀察者角色的UI層即將接收到一個空數據的PagedList。
這沒有任何意義,因此我們更希望PagedList第一次傳遞到UI層級的同時,已經持有了初始的列表數據(即InitialLoadSizeHint);因此,Paging嘗試在后臺線程中通過DataSource對PagedList內部的數據列表進行初始化。
現在,PagedList第一次創建完畢,并持有屬于自己的DataSource和初始的列表數據,通過LiveData這個管道,即將向UI層邁出屬于自己的第一個腳印。
2.UI渲染和分頁加載流程
通過內部線程的切換,PagedList從后臺線程切換到了UI線程,通過LiveData抵達了UI層級,也就是我們通常說的Activity或者Fragment中。
讀者應該有印象,在上文的示例代碼中,Activity觀察到PagedList后,會通過PagedListAdapter.submitList()函數將PagedList進行注入。PagedListAdapter第一次接收到PagedList后,就會對UI進行渲染。
當用戶嘗試對屏幕中的列表進行滾動時,我們接收到了需要加載更多數據的信號,這時,PagedList在內部主動觸發數據的加載,數據源提供了更多的數據,PagedList接收到之后將會主動觸發RecyclerView的更新,用戶通過RecyclerView原生動畫觀察到了更多的列表Item。
3.刷新數據源流程
當數據發生了更新,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,并對列表進行 增量更新。
四、DataSource數據源簡介
Paging分頁組件的設計中,DataSource是一個非常重要的模塊。顧名思義,DataSource中的Key對應數據加載的條件,Value對應數據集的實際類型, 針對不同場景,Paging的設計者提供了三種不同類型的DataSource抽象類:
PositionalDataSource<T>ItemKeyedDataSource<Key, Value>
PageKeyedDataSource<Key, Value>
接下來我們分別對其進行簡單的介紹。
本章節涉及的知識點非常重要,但不作為本文的重點,筆者將在該系列的下一篇文章中針對DataSource的設計與實現進行更細節的探究,歡迎關注。
1.PositionalDataSource
PositionalDataSource?是最簡單的DataSource類型,顧名思義,其通過數據所處當前數據集快照的位置(position)提供數據。
PositionalDataSource?適用于 目標數據總數固定,通過特定的位置加載數據,這里Key是Integer類型的位置信息,并且被內置固定在了PositionalDataSource類中,T即數據的類型。
最容易理解的例子就是本文的聯系人列表,其所有的數據都來自本地的數據庫,這意味著,數據的總數是固定的,我們總是可以根據當前條目的position映射到DataSource中對應的一個數據。
來看Room組件配置的dao對應編譯期生成的源碼:
```java
// 1.Room自動生成了 DataSource.Factory
@Override
public DataSource.FactorygetAllStudent() {
// 2.工廠函數提供了PositionalDataSource
return new DataSource.Factory() {@Overridepublic PositionalDataSourcecreate() {return new PositionalDataSource(__db, _statement, false , "Student") {// ...
};
}
};
}
2.ItemKeyedDataSource
ItemKeyedDataSource適用于目標數據的加載依賴特定條目的信息,比如需要根據第N項的信息加載第N+1項的數據,傳參中需要傳入第N項的某些信息時。
同樣拿聯系人列表舉例,另外的一種分頁加載方式是通過上一個聯系人的name作為Key請求新一頁的數據,因為聯系人name字母排序的原因,DataSource很容易針對一個name檢索并提供接下來新一頁的聯系人數據——比如根據Alice找到下一個用戶Bob(A -> B)。
3.PageKeyedDataSource
更多的網絡請求API中,服務器返回的數據中都會包含一個String類型類似nextPage的字段,以表示當前頁數據的下一頁數據的接口(比如Github的API),這種分頁數據加載的方式正是PageKeyedDataSource的拿手好戲。
這是日常開發中用到最多的DataSource類型,和ItemKeyedDataSource不同的是,前者的數據檢索關系是單個數據與單個數據之間的,后者則是每一頁數據和每一頁數據之間的。
同樣拿聯系人列表舉例,這種分頁加載方式是按照頁碼進行數據加載的,比如一次請求15條數據,服務器返回數據列表的同時會返回下一頁數據的url(或者頁碼),借助該參數請求下一頁數據成功后,服務器又回返回下下一頁的url,以此類推。
總的來說,DataSource針對不同種數據分頁的加載策略提供了不同種的抽象類以方便開發者調用,很多情況下,同樣的業務使用不同的DataSource都能夠實現,開發者按需取用即可。
五、最佳實踐
現在讀者對多種不同的數據源DataSource有了簡單的了解,先拋開 分頁列表 的業務不談,我們思考另外一個問題:
當列表的數據通過多個層級 網絡請求(Network) 和 本地緩存 (Database)進行加載該怎么處理?
回答這個問題,需要先思考另外一個問題:
Network+Database的解決方案有哪些優勢?
1.優勢
讀者認真思考可得,Network+Database的解決方案優點如下:
1、非常優秀的離線模式支持,即使用戶設備并沒有鏈接網絡,本地緩存依然可以帶來非常不錯的使用體驗;
2、數據的快速恢復,如果異常導致App的終止,本地緩存可以對頁面數據進行快速恢復,大幅減少流量的損失,以及加載的時間。
3、兩者的配合的效果總是相得益彰。
看起來Network+Database是一個非常不錯的數據加載方案,那么為什么大多數場景并沒有使用本地緩存呢?
主要原因是開發成本——本地緩存的搭建總是需要額外的代碼,不僅如此,更重要的原因是,數據交互的復雜性也會導致額外的開發成本。
2.復雜的交互模型
為什么說Network+Database會導致 數據交互的復雜性 ?
讓我們回到本文的 聯系人列表 的示例中,這個示例中,所有聯系人數據都來自 本地緩存,因此讀者可以很輕易的構建出該功能的整體結構:
如圖所示,ViewModel中的數據總是由Database提供,如果把數據源從Database換成Network,數據交互的模型也并沒有什么區別—— 數據源總是單一的。
那么,當數據的來源不唯一時——即Network+Database的數據加載方案中會有哪些問題呢?
我們來看看常規的實現方案的數據模型:
如圖所示,ViewModel嘗試加載數據時,總是會先進行網絡判斷,若網絡未連接,則展示本地緩存,否則請求網絡,并且在網絡請求成功時,將數據保存本地。
乍得一看,這種方案似乎并沒有什么問題,實際上卻有兩個非常大的弊端:
2.1 業務并非這么簡單
首先,通過一個boolean類型的值就能代表網絡連接的狀態嗎?顯而易見,答案是否定的。
實際上,在某些業務場景下,服務器的連接狀態可以是更為復雜的,比如接收到了部分的數據包?比如某些情況下網絡請求錯誤,這時候是否需要重新展示本地緩存?
若涉及到網絡請求的重試則更復雜,成功展示網絡數據,再次失敗展示緩存——業務越來越復雜,我們甚至會逐漸沉浸其中無法自拔,最終醒悟,這種數據的交互模型完全不夠用了 。
2.2 無用的本地緩存
另外一個很明顯的弊端則是,當網絡連接狀態良好的時候,用戶看到的數據總是服務器返回的數據。
這種情況下,請求的數據再次存入本地緩存似乎毫無意義,因為網絡環境的通暢,Database中的緩存從來未作為數據源被展示過。
3.使用單一數據源
使用 單一數據源 (single source of truth)的好處不言而喻,正如上文所闡述的,多個數據源 反而會將業務邏輯變得越來越復雜,因此,我們設計出這樣的模型:
ViewModel如果響應Database中的數據變更,且Database作為唯一的數據來源?
其思路是:ViewModel只從Database中取得數據,當Database中數據不夠時,則向Server請求網絡數據,請求成功,數據存入Database,ViewModel觀察到Database中數據的變更,并更新到UI中。
這似乎無法滿足上文中的需求?讀者認真思考可知,其實是沒問題的,當網絡連接發生故障時,這時向服務端請求數據失敗,并不會更新Database,因此UI展示的正是期望的本地緩存。
ViewModel僅僅響應Database中數據的變更,這種使用 單一數據源 的方式讓復雜的業務邏輯簡化了很多。
4.分頁列表的最佳實踐
現在我們理解了 單一數據源 的好處,該方案在分頁組件中也同樣適用,我們唯一需要實現的是,如何主動觸發服務端數據的請求?
這是當然的,因為Database中依賴網絡請求成功之后的數據存儲更新,否則列表所展示的永遠是Database中不變的數據——別忘了,ViewModel和Server之間并沒有任何關系。
針對Database中的數據更新,簡單的方式是 直接進行網絡請求,這種方式使用非常普遍,比如,列表需要下拉刷新,這時主動請求網絡,網絡請求成功后將數據存入數據庫即可,這時ViewModel響應到數據庫中的更新,并將最新的數據更新在UI上。
另外一種方式則和Paging分頁組件本身有關,當列表滾動到指定位置,需要對下一頁數據進行加載時,如何向網絡拉取最新數據?
Paging為此提供了BoundaryCallback類用于配置分頁列表自動請求分頁數據的回調函數,其作用是,當數據庫中最后一項數據被加載時,則會調用其onItemAtEndLoaded函數:
class MyBoundaryCallback(val database : MyLocalCacheval apiService: ApiService) : PagedList.BoundaryCallback<User>() {
override fun onItemAtEndLoaded(itemAtEnd: User) {
// 請求網絡數據,并更新到數據庫中
requestAndAppendData(apiService, database, itemAtEnd)
}
}
BoundaryCallback類為Paging通過Network+Database進行分頁加載的功能完成了最后一塊拼圖,現在,分頁列表所有數據都來源于本地緩存,并且復雜的業務實現起來也足夠靈活。
5.更多優勢
通過Network+Database進行Paging分頁加載還有更多好處,比如更輕易管理分頁列表 額外的狀態 。
不僅僅是分頁列表,這種方案使得所有列表的 狀態管理 的更加容易,筆者為此撰寫了另外一篇文章去闡述它,篇幅所限,本文不進行展開,有興趣的讀者可以閱讀。
Android官方架構組件Paging-Ex:列表狀態的響應式管理
https://juejin.im/post/5ce6ba09e51d4555e372a562
六、總結
本文對Paging進行了系統性的概述,最后,Paging到底是一個什么樣的分頁庫?
首先,它支持Network、Database或者兩者,通過Paging,你可以輕松獲取分頁數據,并直接更新在RecyclerView中。
其次,Paging使用了非常優秀的 觀察者模式 ,其簡單的API的內部封裝了復雜的分頁邏輯。
第三,Paging靈活的配置和強大的支持——不同DataSource的數據加載方式、不同的響應式庫的支持(LiveData、RxJava)等等,Paging總是能夠勝任分頁數據加載的需求。
更多 & 參考
再次重申,強烈建議 讀者將本文作為學習Paging 閱讀優先級最高的文章,所有其它的Paging中文博客閱讀優先級都應該靠后。
——是因為本文的篇幅較長嗎?(1w字的確...)不止如此,本文嘗試對Paging的整體結構進行拆分,筆者認為,只要對整體結構有足夠的理解,一切API的調用都輕而易舉。但如果直接上手寫代碼的話,反而容易造成 只見樹木,不見森林 之感,上手效率反而降低。
推薦閱讀
(點擊標題可跳轉閱讀)
App流暢度優化:利用字節碼插樁實現一個快速排查高耗時方法的工具
談談Android AOP技術方案
代理模式以及在Android中的使用
看完這篇 HTTPS,和面試官扯皮就沒問題了
覺得本文對你有幫助?請分享給更多人
wx號:gulinhai531
顧林海公眾號
不定期推出優質文
章,喜歡的朋友們
給我個好看。
好文章,我在看??
總結
以上是生活随笔為你收集整理的android实现箭头流程列表_反思|Android 列表分页组件Paging的设计与实现:系统概述...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2022年12月全球热门移动游戏下载TO
- 下一篇: android 屏幕分辨率 屏幕密度,A