Android 列表分頁組件Paging的設計與實現(xiàn)
先通過官方Paging示例開始,通過Paging實現(xiàn)加載Room數(shù)據(jù)庫中的聯(lián)系人列表簡單介紹jetpack中的Paging的使用
數(shù)據(jù)庫為Room,于是先定義的數(shù)據(jù)查詢Dao,如下所示:
@Dao
interface CheeseDao {
@Query("select * from cheese order by name ")
fun findAllCheese(): DataSource.Factory<Int, Cheese> //返回的為 DataSource.Factory對象
}
可以看到Room數(shù)據(jù)庫直接返回的為DataSource.Factory而不是livedata<User>,后文會提出來,因為它也可構建出一個可觀察的對象LiveData數(shù)據(jù)。
接下來可查看ViewModel和Activity中的實現(xiàn):
class MainActivity : AppCompatActivity() {
//負責數(shù)據(jù)的類的加載
private val viewModel by viewModels<CheeseViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Create adapter for the RecyclerView
val adapter = CheeseAdapter()
cheeseList.adapter = adapter
// Subscribe the adapter to the ViewModel, so the items in the adapter are refreshed
// 當viewModel中的allCheeses發(fā)生變化后會調用
viewModel.allCheeses.observe(this, Observer {
mAdapter.submitList(it)
})
//...
}
}
在來看看Paging中為RecycleView準備的CheeseAdapter
class CheeseAdapter :
PagedListAdapter<Cheese, CheeseViewHolder>(object : DiffUtil.ItemCallback<Cheese>() {
override fun areItemsTheSame(oldItem: Cheese, newItem: Cheese): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Cheese, newItem: Cheese): Boolean =
oldItem == newItem
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheeseViewHolder =
CheeseViewHolder(parent)
override fun onBindViewHolder(holder: CheeseViewHolder, position: Int) {
holder.bindData(getItem(position))
}
}
這里使用到了PagedListAdapter 需要一個DiffUtil.ItemCallback<T>類型參數(shù),它是官方基于RecyclerView.Adapter的AsyncListDiffer封裝類,其內創(chuàng)建了AsyncListDiffer的示例,以便在后臺線程中使用DiffUtil計算新舊數(shù)據(jù)集的差異,從而節(jié)省Item更新的性能。
viewModel中負責處理數(shù)據(jù),則可以去到CheeseViewModel中,查看數(shù)據(jù)是如何加載,可以看到dao.findAllCheese()是DataSource.Factory對象。通過toLiveData(),傳入Paging所需要的Config,即可完成數(shù)據(jù)的轉化和查找。
class CheeseViewModel(app: Application) : AndroidViewModel(app) {
val dao = CheeseDb.get(app).cheeseDao()
//LiveData類型數(shù)據(jù)
val allCheese = dao.findAllCheese().toLiveData(
Config(
pageSize = 30,
enablePlaceholders = true,
maxSize = 200
)
)
}
以上:則一個Paging最簡單的列表完成,可以看到用的如下幾個核心類:DataSource.Factory 、 PagedListAdapter、 DiffUtil.ItemCallback、PagedListBuilder、DataSource和Room數(shù)據(jù)庫的使用
接下來通過單獨介紹這幾種組件和關系,來探究Paging框架
一、分頁組件的簡介
1.核心類 PagedList
上文提到,一個普通的RecyclerView展示的是一個列表的數(shù)據(jù),比如List,但在列表分頁的需求中,列表局部更新或者差分異比對,顯然一個List不太夠用了。
為此,Google設計出了一個新的角色PagedList,顧名思義,該角色的意義就是 分頁列表數(shù)據(jù)的容器 。
既然有了List,為什么需要額外設計這樣一個PagedList的數(shù)據(jù)結構?本質原因在于加載分頁數(shù)據(jù)的操作是異步的 ,因此定義PagedList的第二個作用是 對分頁數(shù)據(jù)的異步加載 ,這個我們后文再提。
所以ViewModel可以定義成這樣,因為PagedList也作為列表數(shù)據(jù)的容器(就像List一樣):
class viewModel :viewModel(){
//before
//val users :LiveData<List<User>> = dao.findAllUsers()
//after
val users:LiveData<PagedList<User>> = dao.findAllUsers()
}
在ViewModel中,開發(fā)者可以輕易通過對users進行訂閱以響應分頁數(shù)據(jù)的更新,這個LiveData的可觀察者是通過Room組件創(chuàng)建的,我們來看一下我們的dao:
@Dao
interface UserDao {
// 注意,這里 LiveData<List<User>> 改成了 LiveData<PagedList<User>>
@Query("SELECT * FROM user")
fun queryUsers(): LiveData<PagedList<User>>
}
乍得一看似乎理所當然,但實際需求中有一個問題,這里的定義是模糊不清的——對于分頁數(shù)據(jù)而言,不同的業(yè)務場景,所需要的相關配置是不同的。那么什么是分頁相關配置呢?
最直接的一點是每頁數(shù)據(jù)的加載數(shù)量PageSize,不同的項目都會自行規(guī)定每頁數(shù)據(jù)量的大小,一頁請求15個數(shù)據(jù)還是20個數(shù)據(jù)?所以接下來DataSource和PagedListBuilder對象,通過簡單的配置將數(shù)據(jù)源和分頁Page的相關屬性。
2.數(shù)據(jù)源:DataSource及其工廠
回答這個問題之前,我們還需要定義一個角色,用來為PagedList容器提供分頁數(shù)據(jù),那就是數(shù)據(jù)源DataSource。
什么是DataSource呢?可以理解為 數(shù)據(jù)庫數(shù)據(jù) 或者是 服務端數(shù)據(jù) 的一個快照,而不應該是數(shù)據(jù)庫數(shù)據(jù)或者是服務端數(shù)據(jù)
每當Paging被告知需要更多的數(shù)據(jù)的時候,數(shù)據(jù)源DataSource就會將當前的快照對應的索引的數(shù)據(jù)交給PagedList處理
但是需要構建一個新的PagedList的時候,比如數(shù)據(jù)已經失效,DataSource中舊的數(shù)據(jù)就有意義了,因為DataSource需要被重置
在代碼中,這意味著新的DataSource對象被創(chuàng)建,因此,我們需要提供的不是DataSource,而是提供DataSource的工廠(DataSouce.Factory) 這就是為什么查找數(shù)據(jù)庫的時候,返回的事DataSouce.Factory而不是DataSouce<PageList<User>>或者是LiveData<PageList<User>>的原因
為什么要提供DataSource.Factory而不是一個DataSource? 復用這個DataSource不可以嗎,當然可以,但是將DataSource設置為immutable(不可變)會避免更多的未知因素。
接下來如何修改方法中放回的類型,如下所示:
@Dao
interface UserDao{
@Query("select * from user")
fun findAllUser():DataSource.Factory<Int,User>
}
返回的是一個數(shù)據(jù)源的提供者DataSource.Factory,頁面初始化時,會通過工廠方法創(chuàng)建一個新的DataSource,這之后對應會創(chuàng)建一個新的PagedList,每當PagedList想要獲取下一頁的數(shù)據(jù),數(shù)據(jù)源都會根據(jù)請求索引進行數(shù)據(jù)的提供。
當數(shù)據(jù)失效時,DataSource.Factory會再次創(chuàng)建一個新的DataSource,其內部包含了最新的數(shù)據(jù)快照(本案例中代表著數(shù)據(jù)庫中的最新數(shù)據(jù)),隨后創(chuàng)建一個新的PagedList,并從DataSource中取最新的數(shù)據(jù)進行展示——當然,這之后的分頁流程都是相同的,無需再次復述。
引用一幅圖用于描述三者之間的關系,讀者可參考上述文字和圖片加以理解

3.串聯(lián)兩者:PagedListBuilder
分頁中的相關業(yè)務配置,如每次加載多少條數(shù)據(jù)等等
現(xiàn)在在Dao中接口的返回值已經是DataSource.Factory,而ViewModel中的成員被觀察者則是LiveData<PagedList<User>>類型,那么如何將數(shù)據(jù)源的工廠DataSource.Factory,和LiveData<PagedList>進行串聯(lián)?
因此需要定義一個新的角色PagedListBuilder,開發(fā)者將 數(shù)據(jù)源工廠 和 相關配置統(tǒng)一交給PagedListBuilder,即可生成對應的LiveData<PagedList<User>>:
class MyViewModel(val dao: UserDao) : ViewModel() {
val users: LiveData<PagedList<User>>
init {
// 1.創(chuàng)建DataSource.Factory
val factory: DataSource.Factory = dao.queryUsers()
// 2.通過LivePagedListBuilder配置工廠和pageSize, 對users進行實例化
// users = LivePagedListBuilder(factory, config).build()
// 也可以是具體的config對象,定制更多的配置參數(shù)
users = LivePagedListBuilder(factory, 30).build()
}
}
如代碼所示:在viewmodel中先通過dao獲取到DataSource.Factory,工廠創(chuàng)建數(shù)據(jù)源DataSource,后者為PagedList提供列表所需要的數(shù)據(jù);此外,另外一個Int類型的參數(shù)則制定每頁數(shù)據(jù)加載的數(shù)量,這里指定數(shù)量為30
所以在viewmodel中創(chuàng)建了一個LiveData<PagedList<User>> 的可觀察對象,則在Actiivty中的代碼如下所示:
class MyActivity : Activity {
val myViewModel: MyViewModel
// 1.這里我們使用PagedListAdapter
val adapter: PagedListAdapter
fun onCreate(bundle: Bundle?) {
// 2.在Activity中對LiveData進行訂閱
myViewModel.users.observe(this) {
// 3.每當數(shù)據(jù)更新,計算新舊數(shù)據(jù)集的差異,對列表進行更新
adapter.submitList(it)
}
}
}
4.更多可選的配置:PagedList.Config
目前介紹中,分頁的功能大致已經介紹完成,但是這些在現(xiàn)實開發(fā)中往往不夠,因此,設計者額外定義了更復雜的數(shù)據(jù)結構PagedList.Config,以描述更細節(jié)化的配置參數(shù)
// after
val config = PagedList.Config.Builder()
.setPageSize(15) // 分頁加載的數(shù)量
.setInitialLoadSizeHint(30) // 初次加載的數(shù)量
.setPrefetchDistance(10) // 預取數(shù)據(jù)的距離
.setEnablePlaceholders(false) // 是否啟用占位符
.build()
// API發(fā)生了改變
val users: LiveData<PagedList<User>> = LivePagedListBuilder(factory, config).build()
4.1.分頁數(shù)量:PageSize
最易理解的配置,分頁請求數(shù)據(jù)時,開發(fā)者總是需要定義每頁加載數(shù)據(jù)的數(shù)量。
4.2.初始加載數(shù)量:InitialLoadSizeHint
定義首次加載時要加載的Item數(shù)量。
此值通常大于PageSize,因此在初始化列表時,該配置可以使得加載的數(shù)據(jù)保證屏幕可以小范圍的滾動。
如果未設置,則默認為PageSize的三倍。
4.3.預取距離:PrefetchDistance
顧名思義,該參數(shù)配置定義了列表當距離加載邊緣多遠時進行分頁的請求,默認大小為PageSize——即距離底部還有一頁數(shù)據(jù)時,開啟下一頁的數(shù)據(jù)加載。
若該參數(shù)配置為0,則表示除非明確要求,否則不會加載任何數(shù)據(jù),通常不建議這樣做,因為這將導致用戶在滾動屏幕時看到占位符或列表的末尾。
4.4.是否啟用占位符:PlaceholderEnabled
該配置項需要傳入一個boolean值以決定列表是否開啟placeholder(占位符),在知道DataSource知道總數(shù)的情況下,設置為true,則可實現(xiàn)骨架屏的效果
4.5 更多觀察者類型的配置
在本文的示例中,我們建立了一個LiveData>的可觀察者對象供用戶響應數(shù)據(jù)的更新,實際上組件的設計應該面向提供對更多優(yōu)秀異步庫的支持,比如RxJava。
因此,和LivePagedListBuilder一樣,設計者還提供了RxPagedListBuilder,通過DataSource數(shù)據(jù)源和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()
二、DataSource數(shù)據(jù)源簡介
ItemKeyedDataSource<Key, Value>, PageKeyedDataSource<Key, Value>, PositionalDataSource<T>
Base class for loading pages of snapshot data into a PagedList.
DataSource is queried to load pages of content into a PagedList. A PagedList can grow as it loads more data, but the data loaded cannot be updated. If the underlying data set is modified, a new PagedList / DataSource pair must be created to represent the new data.
用于將快照數(shù)據(jù)頁加載到PagedList的基類。
查詢數(shù)據(jù)源以將內容頁加載到PagedList中。頁面列表可以隨著加載更多數(shù)據(jù)而增長,但無法更新加載的數(shù)據(jù)。如果修改了基礎數(shù)據(jù)集,則必須創(chuàng)建一個新的pagelist/DataSource對來表示新數(shù)據(jù)。
Paging分頁組件的設計中,DataSource是一個非常重要的模塊。顧名思義,DataSource中的Key對應數(shù)據(jù)加載的條件,Value對應數(shù)據(jù)集的實際類型, 針對不同場景,Paging的設計者提供了三種不同類型的DataSource抽象類:
PositionalDataSourceItemKeyedDataSourcePageKeyedDataSource
接下來我們分別對其進行簡單的介紹。
1.PositionalDataSource
PositionalDataSource是最簡單的DataSource類型,顧名思義,其通過數(shù)據(jù)所處當前數(shù)據(jù)集快照的位置(position)提供數(shù)據(jù)。
PositionalDataSource適用于 目標數(shù)據(jù)總數(shù)固定,通過特定的位置加載數(shù)據(jù),這里Key是Integer類型的位置信息,并且被內置固定在了PositionalDataSource類中,T即數(shù)據(jù)的類型。
最容易理解的例子就是本文的聯(lián)系人列表,其所有的數(shù)據(jù)都來自本地的數(shù)據(jù)庫,這意味著,數(shù)據(jù)的總數(shù)是固定的,我們總是可以根據(jù)當前條目的position映射到DataSource中對應的一個數(shù)據(jù)。
PositionalDataSource也正是Room幕后實現(xiàn)的功能,使用Room為什么可以避免DataSource的配置,通過dao中的接口就能返回一個DataSource.Factory?
來看Room組件配置的dao對應編譯期生成的源碼:
// 1.Room自動生成了 DataSource.Factory
@Override
public DataSource.Factory<Integer, Student> getAllStudent() {
// 2.工廠函數(shù)提供了PositionalDataSource
return new DataSource.Factory<Integer, Student>() {
@Override
public PositionalDataSource<Student> create() {
return new PositionalDataSource<Student>(__db, _statement, false , "Student") {
// ...
};
}
};
}
2.ItemKeyedDataSource
ItemKeyedDataSource適用于目標數(shù)據(jù)的加載依賴特定條目的信息,比如需要根據(jù)第N項的信息加載第N+1項的數(shù)據(jù),傳參中需要傳入第N項的某些信息時。
使用場景:如QQ或者wechat中的聊天記錄
3.PageKeyedDataSource
這也是最常用的DataSource,更多的用于網絡請求API中,服務器返回的數(shù)據(jù)中都會包含一個String類型類似nextPage的字段,以表示當前頁數(shù)據(jù)的下一頁數(shù)據(jù)的接口(比如Github的API),這種分頁數(shù)據(jù)加載的方式正是PageKeyedDataSource的拿手好戲。
這是日常開發(fā)中用到最多的DataSource類型,和ItemKeyedDataSource不同的是,前者的數(shù)據(jù)檢索關系是單個數(shù)據(jù)與單個數(shù)據(jù)之間的,后者則是每一頁數(shù)據(jù)和每一頁數(shù)據(jù)之間的。
同樣拿聯(lián)系人列表舉例,這種分頁加載方式是按照頁碼進行數(shù)據(jù)加載的,比如一次請求15條數(shù)據(jù),服務器返回數(shù)據(jù)列表的同時會返回下一頁數(shù)據(jù)的url(或者頁碼),借助該參數(shù)請求下一頁數(shù)據(jù)成功后,服務器又回返回下下一頁的url,以此類推。
總的來說,DataSource針對不同種數(shù)據(jù)分頁的加載策略提供了不同種的抽象類以方便開發(fā)者調用,很多情況下,同樣的業(yè)務使用不同的DataSource都能夠實現(xiàn),開發(fā)者按需取用即可。
三、通過Paging加載網絡數(shù)據(jù)列表
通過以上,相信讀者能明白Paging中的核心類的認識和作用,接下來,通過Paging加載一個簡單的網絡列表,具體的實現(xiàn)自定義DataSource和Repository等,更加深刻的理解Paging框架。
請求地址為:https://www.wanandroid.com/article/list/0/json (感謝玩Android)
效果圖如下圖所示:

- Activity中的代碼如下
class NetPagingActivity : AppCompatActivity() {
lateinit var mAdapter: ArticleAdapter
//加載數(shù)據(jù)使用的ViewModel對象
val viewModel: ArticleViewModel by viewModels<ArticleViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
//為RecycleView設置的adapter
mAdapter = ArticleAdapter()
rv_as_article.layoutManager = LinearLayoutManager(
this, LinearLayoutManager.VERTICAL, false
)
rv_as_article.adapter = mAdapter
getData()
}
//請求數(shù)據(jù),并更新列表
private fun getData() {
viewModel.data.observe(this, Observer {
mAdapter.submitList(it)
})
}
}
- ArticleViewModel
ViewModel很簡單,通過NetRepository().getData()獲取DataSource中的可觀察數(shù)據(jù)
class ArticleViewModel : ViewModel() {
val data = NetRepository().getData()
}
- NetRepository倉庫
class NetRepository {
var pageSize = 20
lateinit var article: LiveData<PagedList<Article>>
fun getData(): LiveData<PagedList<Article>> {
val dataSourceFactory = NetDataSourceFactory()
article = dataSourceFactory.toLiveData(
config = Config(
pageSize = pageSize,
enablePlaceholders = false,
initialLoadSizeHint = pageSize * 2
)
)
return article
}
}
- NetDataSourceFactory
通過繼承DataSource.Factory.重寫onCreate()方法,即構建出一個 DataSource對象
class NetDataSourceFactory() : DataSource.Factory<Int, Article>() {
val sourceLiveData = MutableLiveData<NetDataSource>()
override fun create(): DataSource<Int, Article> {
//NetDataSource為具體加載服務器數(shù)據(jù)的快照
val source = NetDataSource()
sourceLiveData.postValue(source)
return source
}
}
- NetDataSource
通過繼承PageKeyedDataSource,因為請求的列表是根據(jù)nextPage來定位查找,所以選中PageKeyedDataSource。
class NetDataSource : PageKeyedDataSource<Int, Article>() {
var pageNo = 0
@SuppressLint("CheckResult")
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, Article>
) {
RedditApi.create().getArticles(pageNo)
.subscribeOn(Schedulers.io())
.subscribe {
it.data?.datas?.let { it1 ->
callback.onResult(it1, pageNo, it.data?.curPage)
}
pageNo = it.data?.curPage!!
}
}
@SuppressLint("CheckResult")
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Article>) {
RedditApi.create().getArticles(pageNo)
.subscribeOn(Schedulers.io())
.subscribe {
callback.onResult(it.data?.datas!!, it.data?.curPage)
pageNo = it.data?.curPage!!
}
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Article>) {
}
}
- ArticleAdapter
通過繼承子基于RecycleView的PagedListAdapter
class ArticleAdapter : PagedListAdapter<Article, ArticleViewHolder>(diffCallback) {
companion object {
val diffCallback = object : DiffUtil.ItemCallback<Article>() {
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean =
oldItem == newItem
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean =
oldItem.id == newItem.id
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
return ArticleViewHolder(parent)
}
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
holder.bindData(getItem(position))
}
}
至此,Paging框架已介紹完成,待后續(xù)更新Jetpack更多的組件!
文章中的所有示例代碼已上傳至github:https://github.com/OnexZgj/Jetpack_Component