技術(shù)不止,文章有料,加
JiuXinDev入群,Android 搬磚路上不孤單
前言
又到了學(xué)習(xí) Android Jetpack 的時(shí)間了,之前我已經(jīng)寫過了一篇《即學(xué)即用Android Jetpack - Paging》,不過Android Jetpack 一直都在更新,這不,Paging 2 已經(jīng)升級(jí)到了 Paging 3,那么,大家可能有很多要關(guān)注的問題,比如:
- Paging 3 到底升級(jí)了那些東西?
- 如何使用最新的 Paging 3?
這是我本期要和大家討論的東西。本期的最終效果:

如果想學(xué)習(xí) Android Jetpack,可以參考我之前的文章《學(xué)習(xí)Android Jetpack? 實(shí)戰(zhàn)和教程這里全都有!》,這已經(jīng)是 《即學(xué)即用Android Jetpack》系列的第八篇啦~
本篇的源碼地址:【Hoo】
目錄

一、Paging 2 和 Paging 3 有什么不同?
如果你沒有使用過 Paging 2,那么你可以跳過本章節(jié)(友情提醒~)。
如果你使用過 Paging 2,你會(huì)發(fā)現(xiàn) Paging 3 簡(jiǎn)直是大刀闊斧,很多 API 的使用方式都變了,簡(jiǎn)單說一下主要改變的東西:
- 支持 Kotlin 中的 Flow。
- 簡(jiǎn)化數(shù)據(jù)源
PagingSource的實(shí)現(xiàn)。 - 增加請(qǐng)求數(shù)據(jù)時(shí)狀態(tài)的回調(diào),支持設(shè)置 Header 和 Footer。
- 支持多種方式請(qǐng)求數(shù)據(jù),比如網(wǎng)絡(luò)請(qǐng)求和數(shù)據(jù)庫請(qǐng)求。
二、介紹
友情提示
官方文檔:Paging 3
谷歌實(shí)驗(yàn)室:官方教程
官方Demo:Github倉庫查詢Demo
1. 定義
看一下官方的定義:
The Paging library helps you load and display pages of data from a larger dataset from local storage or over network.
就是幫助你采用分頁的方式解決顯示包含本地?cái)?shù)據(jù)庫和網(wǎng)絡(luò)的大數(shù)據(jù)集的問題。
2. 優(yōu)點(diǎn)
Paging 3 的優(yōu)點(diǎn):
- 使用內(nèi)存幫你緩存數(shù)據(jù)。
- 內(nèi)置請(qǐng)求去重,幫助你更有效率的顯示數(shù)據(jù)。
- 自動(dòng)發(fā)起請(qǐng)求當(dāng)
RecyclerView滑到底部的時(shí)候。 - 支持 Kotlin 中的
協(xié)程和Flow,還有LiveData和RxJava2。 - 內(nèi)置狀態(tài)處理,包括刷新、錯(cuò)誤、加載等狀態(tài)。
3. 幾個(gè)重要的類
先看一下結(jié)構(gòu):

里面幾個(gè)類的作用:
-
PagingSource:?jiǎn)我坏臄?shù)據(jù)源。 -
RemoteMediator:其實(shí)RemoteMediator也是單一的數(shù)據(jù)源,它會(huì)在PagingSource沒有數(shù)據(jù)的時(shí)候,再使用RemoteMediator提供的數(shù)據(jù),如果既存在數(shù)據(jù)庫請(qǐng)求,又存在網(wǎng)絡(luò)請(qǐng)求,通常PagingSource用于進(jìn)行數(shù)據(jù)庫請(qǐng)求,RemoteMediator進(jìn)行網(wǎng)絡(luò)請(qǐng)求。 -
PagingData:?jiǎn)未畏猪摂?shù)據(jù)的容器。 -
Pager:用來構(gòu)建Flow<PagingData>的類,實(shí)現(xiàn)數(shù)據(jù)加載完成的回調(diào)。 -
PagingDataAdapter:分頁加載數(shù)據(jù)的RecyclerView的適配器。
簡(jiǎn)述一下就是 PagingSource 和 RemoteMediator 充當(dāng)數(shù)據(jù)源的角色,ViewModel 使用 Pager 中提供的 Flow<PagingData> 監(jiān)聽數(shù)據(jù)刷新,每當(dāng) RecyclerView 即將滾動(dòng)到底部的時(shí)候,就會(huì)有新的數(shù)據(jù)的到來,最后,PagingAdapter 復(fù)雜展示數(shù)據(jù)。
三、實(shí)戰(zhàn)
為了簡(jiǎn)單起見,先從單一的數(shù)據(jù)源開始。
第一步 引入依賴
dependencies {
def paging_version = "3.0.0-alpha08"
implementation "androidx.paging:paging-runtime:$paging_version"
// 用于測(cè)試
testImplementation "androidx.paging:paging-common:$paging_version"
// [可選] RxJava 支持
implementation "androidx.paging:paging-rxjava2:$paging_version"
// ... 其他配置不重要,具體可以查看官方文檔
}
第二步 配置數(shù)據(jù)源
在 Paging 2 中,里面有三種 Page Source,開發(fā)者使用的時(shí)候,還得去思考對(duì)應(yīng)的場(chǎng)景,想的腦瓜子疼。
在 Paging 3 中,如果只有單一的數(shù)據(jù)源,那么你要做的很簡(jiǎn)單,繼承 PagingSource 即可。
private const val SHOE_START_INDEX = 0;
class CustomPageDataSource(private val shoeRepository: ShoeRepository) : PagingSource<Int, Shoe>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Shoe> {
val pos = params.key ?: SHOE_START_INDEX
val startIndex = pos * params.loadSize + 1
val endIndex = (pos + 1) * params.loadSize
return try {
// 從數(shù)據(jù)庫拉去數(shù)據(jù)
val shoes = shoeRepository.getPageShoes(startIndex.toLong(), endIndex.toLong())
// 返回你的分頁結(jié)果,并填入前一頁的 key 和后一頁的 key
LoadResult.Page(
shoes,
if (pos <= SHOE_START_INDEX) null else pos - 1,
if (shoes.isNullOrEmpty()) null else pos + 1
)
}catch (e:Exception){
LoadResult.Error(e)
}
}
}
正常用 LoadResult.Page() 返回結(jié)果,出現(xiàn)錯(cuò)誤的情況用 LoadResult.Error 返回錯(cuò)誤。
第三步 生成可觀察的數(shù)據(jù)集
這里可觀察數(shù)據(jù)集包括 LiveData 、Flow 以及 RxJava 中的 Observable 和 Flowable,其中,RxJava 需要單獨(dú)引入擴(kuò)展庫去支持的。
這里的可觀察數(shù)據(jù)集 shoes 使用的是 Flow:
class ShoeModel constructor(private val shoeRepository: ShoeRepository) : ViewModel() {
/**
* @param config 分頁的參數(shù)
* @param pagingSourceFactory 單一數(shù)據(jù)源的工廠,在閉包中提供一個(gè)PageSource即可
* @param remoteMediator 同時(shí)支持網(wǎng)絡(luò)請(qǐng)求和數(shù)據(jù)庫請(qǐng)求的數(shù)據(jù)源
* @param initialKey 初始化使用的key
*/
var shoes = Pager(config = PagingConfig(
pageSize = 20
, enablePlaceholders = false
, initialLoadSize = 20
), pagingSourceFactory = { CustomPageDataSource(shoeRepository) }).flow
// ... 省略
}
可以看到,我們中了的 shoes: Flow<PagingData<Shoe>> 是由 Pager().flow 提供的。Pager 中的參數(shù)稍微解釋一下:
-
PagerConfig可以提供分頁的參數(shù),比如每一頁的加載數(shù)、初始加載數(shù)和最大數(shù)等。 -
pagingSourceFactory和remoteMediator都是數(shù)據(jù)源,我們使用其中的一個(gè)即可。
第四步 創(chuàng)建Adapter
和普通的 Adapter 沒有特別大的區(qū)別,主要是:
- 繼承
PagingDataAdapter - 提供
DiffUtil.ItemCallback<Shoe>
實(shí)際代碼:
/**
* 鞋子的適配器 配合Data Binding使用
*/
class ShoeAdapter constructor(val context: Context) :
PagingDataAdapter<Shoe, ShoeAdapter.ViewHolder>(ShoeDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ShoeRecyclerItemBinding.inflate(
LayoutInflater.from(parent.context)
, parent
, false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val shoe = getItem(position)
holder.apply {
bind(onCreateListener(shoe!!.id), shoe)
itemView.tag = shoe
}
}
/**
* Holder的點(diǎn)擊事件
*/
private fun onCreateListener(id: Long): View.OnClickListener {
return View.OnClickListener {
val intent = Intent(context, DetailActivity::class.java)
intent.putExtra(BaseConstant.DETAIL_SHOE_ID, id)
context.startActivity(intent)
}
}
class ViewHolder(private val binding: ShoeRecyclerItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(listener: View.OnClickListener, item: Shoe) {
binding.apply {
this.listener = listener
this.shoe = item
executePendingBindings()
}
}
}
}
class ShoeDiffCallback: DiffUtil.ItemCallback<Shoe>() {
override fun areItemsTheSame(oldItem: Shoe, newItem: Shoe): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Shoe, newItem: Shoe): Boolean {
return oldItem == newItem
}
}
第五步 在UI中使用
先來簡(jiǎn)單一點(diǎn)的,如果只顯示數(shù)據(jù),我們要做的是:
- 創(chuàng)建和設(shè)置適配器。
- 開啟一個(gè)協(xié)程
- 在協(xié)程中接收
Flow提供的數(shù)據(jù)。
我的代碼:
private fun onSubscribeUi(binding: ShoeFragmentBinding) {
binding.lifecycleOwner = this
// 初始化RecyclerView部分
val adapter = ShoeAdapter(context!!)
binding.recyclerView.adapter = adapter
job = viewModel.viewModelScope.launch(Dispatchers.IO) {
viewModel.shoes.collect() {
adapter.submitData(it)
}
}
// ... 省略
}
第六步 設(shè)置Header和Footer
顯然 Paging 3 還有更多的功能,對(duì)!它還支持添加 Header 和 Footer,官方示例是把它們用作上拉刷新和下拉加載更多的控件。

不過 Hoo 中并沒有使用 Header 和 Footer,我們來看看官方怎么使用的:
- 創(chuàng)建一個(gè)
HeaderAdapter or FooterAdapter繼承自LoadStateAdapter,跟普通Adapter不一樣的地方在于它在onBindViewHolder方法中提供了LoadState參數(shù),它可以提供當(dāng)前Paging是 Loading、NotLoading 和 Error 的狀態(tài)。 - 跟普通
Adapter一樣創(chuàng)建自己需要的ViewHolder。 - 稍微修改一下第五步中設(shè)置
ShoeAdapter,調(diào)用它的withLoadStateHeaderAndFooter方法綁定Header和Footer的適配器:
private fun onSubscribeUi(binding: ShoeFragmentBinding) {
binding.lifecycleOwner = this
// 初始化RecyclerView部分
val adapter = ShoeAdapter(context!!).withLoadStateHeaderAndFooter(
header = ReposLoadStateAdapter { adapter.retry() },
footer = ReposLoadStateAdapter { adapter.retry() }
)
binding.recyclerView.adapter = adapter
// ... 省略
}
需要說明一下,我這里使用的偽代碼。想要看詳細(xì)的教程,可以官網(wǎng)的這一步教程。
第七步 監(jiān)聽數(shù)據(jù)加載的狀態(tài)
PagingDataAdapter 除了添加 Header 和 Footer,還可以監(jiān)聽數(shù)據(jù)的加載狀態(tài),狀態(tài)對(duì)應(yīng)的類是 LoadState,它有三種狀態(tài):
-
Loading:數(shù)據(jù)加載中。 -
NotLoading:內(nèi)存中有已經(jīng)獲取的數(shù)據(jù),即使往下滑,Paging 也不需要請(qǐng)求更多的數(shù)據(jù)。 -
Error:請(qǐng)求數(shù)據(jù)時(shí)返回了一個(gè)錯(cuò)誤。
監(jiān)聽數(shù)據(jù)狀態(tài)的代碼:
adapter.addLoadStateListener {state:CombinedLoadStates->
//... 狀態(tài)監(jiān)聽
}
監(jiān)聽方法就是這么簡(jiǎn)單,可以看到這個(gè) state 并不是 LoadState,而是一個(gè) CombinedLoadStates,顧名思義,就是多個(gè) LoadState 組合而成的狀態(tài)類,它里面有:
-
refresh:LoadState:刷新時(shí)的狀態(tài),因?yàn)榭梢哉{(diào)用PagingDataAdapter#refresh()方法進(jìn)行數(shù)據(jù)刷新。 -
append:LoadState:可以理解為RecyclerView向下滑時(shí)數(shù)據(jù)的請(qǐng)求狀態(tài)。 -
prepend:LoadState:可以理解為RecyclerView向上滑時(shí)數(shù)據(jù)的請(qǐng)求狀態(tài)。 -
source和mediator分別包含上面123的屬性,source代表單一的數(shù)據(jù)源,mediator代表多數(shù)據(jù)源的場(chǎng)景,source和mediator二選一。
解釋了這么多,說一下我的玩法,下拉刷新我使用了第三方的刷新控件 SmartRefreshLayout,這就意味著我要自己處理 SmartRefreshLayout 的加載狀態(tài)。布局文件不貼了,就是 SmartRefreshLayout + RecyclerView + FloatingActionButton。
成果圖(Gif有一點(diǎn)點(diǎn)問題):

使用代碼就是:
private fun onSubscribeUi(binding: ShoeFragmentBinding) {
binding.lifecycleOwner = this
// 初始化RecyclerView部分
val adapter = ShoeAdapter(context!!)
// 數(shù)據(jù)加載狀態(tài)的回調(diào)
adapter.addLoadStateListener { state:CombinedLoadStates ->
currentStates = state.source
// 如果append沒有處于加載狀態(tài),但是refreshLayout出于加載狀態(tài),refreshLayout停止加載狀態(tài)
if (state.append is LoadState.NotLoading && binding.refreshLayout.isLoading) {
refreshLayout.finishLoadMore()
}
// 如果refresh沒有出于加載狀態(tài),但是refreshLayout出于刷新狀態(tài),refreshLayout停止刷新
if (state.source.refresh is LoadState.NotLoading && binding.refreshLayout.isRefreshing) {
refreshLayout.finishRefresh()
}
}
binding.recyclerView.adapter = adapter
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastPos = getLastVisiblePosition(binding.recyclerView)
if (!(lastPos == adapter.itemCount - 1 && currentStates?.append is LoadState.Loading)) {
binding.refreshLayout.finishLoadMore()
}
}
})
job = viewModel.viewModelScope.launch(Dispatchers.IO) {
viewModel.shoes.collect {
adapter.submitData(it)
}
}
binding.refreshLayout.setRefreshHeader(DropBoxHeader(context))
binding.refreshLayout.setRefreshFooter(ClassicsFooter(context))
binding.refreshLayout.setOnLoadMoreListener {
// 如果當(dāng)前數(shù)據(jù)已經(jīng)全部加載完,就不再加載
if(currentStates?.append?.endOfPaginationReached == true)
binding.refreshLayout.finishLoadMoreWithNoMoreData()
}
//... 省略無關(guān)代碼
}
到這兒,正常的流程就走通了。
四、更多
1. 使用RemoteMediator
RemoteMediator 的使用機(jī)制跟 PageSource 沒什么區(qū)別,默認(rèn)是先走 PageSource 的數(shù)據(jù),如果 PageSource 提供的數(shù)據(jù)返回為空的情況,才會(huì)走 RemoteMediator 的數(shù)據(jù),所以你可以用:
-
PageSource:進(jìn)行數(shù)據(jù)庫請(qǐng)求提供數(shù)據(jù)。 -
RemoteMediator:進(jìn)行網(wǎng)絡(luò)請(qǐng)求,然后對(duì)請(qǐng)求成功的數(shù)據(jù)進(jìn)行數(shù)據(jù)庫的存儲(chǔ)。
鑒于使用 RemoteMediator 跟普通的方式?jīng)]什么本質(zhì)區(qū)別,我這里就不再做更多介紹,感興趣的同學(xué)可以查看文檔。
2. 談?wù)勎矣龅降囊恍┛?/h4>
界面

之前點(diǎn)擊品牌按鈕, Paing 2 對(duì)應(yīng)的版本使用的 LiveData,這是我之前的寫法:
class ShoeModel constructor(shoeRepository: ShoeRepository) : ViewModel() {
// 品牌的觀察對(duì)象 默認(rèn)觀察所有的品牌
private val brand = MutableLiveData<String>().apply {
value = ALL
}
// 鞋子集合的觀察類
val shoes: LiveData<PagedList<Shoe>> = brand.switchMap {
// Room數(shù)據(jù)庫查詢,只要知道返回的是LiveData<List<Shoe>>即可
if (it == ALL) {
// LivePagedListBuilder<Int,Shoe>( shoeRepository.getAllShoes(),PagedList.Config.Builder()
LivePagedListBuilder<Int, Shoe>(
CustomPageDataSourceFactory(shoeRepository) // DataSourceFactory
, PagedList.Config.Builder()
.setPageSize(10) // 分頁加載的數(shù)量
.setEnablePlaceholders(false) // 當(dāng)item為null是否使用PlaceHolder展示
.setInitialLoadSizeHint(10) // 預(yù)加載的數(shù)量
.build()
)
.build()
//shoeRepository.getAllShoes()
} else {
val array: Array<String> =
when (it) {
NIKE -> arrayOf("Nike", "Air Jordan")
ADIDAS -> arrayOf("Adidas")
else -> arrayOf(
"Converse", "UA"
, "ANTA"
)
}
shoeRepository.getShoesByBrand(array)
.createPagerList(6, 6)
}
}
fun setBrand(brand: String) {
this.brand.value = brand
}
companion object {
public const val ALL = "所有"
public const val NIKE = "Nike"
public const val ADIDAS = "Adidas"
public const val OTHER = "other"
}
}
切換到 Paging 3 的時(shí)候遇到一些坑:
先把可觀察的數(shù)據(jù)集從 Flow 轉(zhuǎn)到 LiveData,也就是從 Pager(...).flow 到 Pager(...).livedata:
class ShoeModel constructor(private val shoeRepository: ShoeRepository) : ViewModel() {
/**
* @param config 分頁的參數(shù)
* @param pagingSourceFactory 單一數(shù)據(jù)源的工廠,在閉包中提供一個(gè)PageSource即可
* @param remoteMediator 同時(shí)支持網(wǎng)絡(luò)請(qǐng)求和數(shù)據(jù)庫請(qǐng)求的數(shù)據(jù)源
* @param initialKey 初始化使用的key
*/
var shoes = Pager(config = PagingConfig(
pageSize = 20
, enablePlaceholders = false
, initialLoadSize = 20
), pagingSourceFactory = { CustomPageDataSource(shoeRepository) }).liveData
//....
}
緊接著,我在 ShoeFragment 進(jìn)行數(shù)據(jù)的監(jiān)聽:
job = viewModel.viewModelScope.launch() {
viewModel.shoes.observe(viewLifecycleOwner, Observer<PagingData<Shoe>> {
adapter.submitData(viewLifecycleOwner.lifecycle,it)
})
}
第一個(gè)坑出現(xiàn)了,在 Paging 2 中,PagingSouce 獲取數(shù)據(jù)是處在子線程的,而 Paging 3 中,獲取數(shù)據(jù)處在當(dāng)前線程,不會(huì)做線程切換,如果直接使用數(shù)據(jù)庫查詢,會(huì)報(bào)錯(cuò),因?yàn)?Room 中規(guī)定,數(shù)據(jù)庫查詢不能在主線程。
好家伙,既然主線程不可以請(qǐng)求數(shù)據(jù)庫,那么我就把它放在IO線程,在 launch 方法中加上 Dispatchers.IO:
job = viewModel.viewModelScope.launch(Dispatchers.IO) {
viewModel.shoes.observe(viewLifecycleOwner, Observer<PagingData<Shoe>> {
adapter.submitData(viewLifecycleOwner.lifecycle,it)
})
}
第二個(gè)坑安排上,LiveData#observe(...) 方法不能發(fā)生在后臺(tái)線程。

這就沒辦法了,只能使用 Flow 去實(shí)現(xiàn),不過 Flow 也有坑,當(dāng)數(shù)據(jù)源發(fā)生變化的時(shí)候,數(shù)據(jù)需要重新監(jiān)聽,代碼也沒有了之前的優(yōu)雅:
class ShoeModel constructor(private val shoeRepository: ShoeRepository) : ViewModel() {
var shoes = Pager(config = PagingConfig(
pageSize = 20
, enablePlaceholders = false
, initialLoadSize = 20
), pagingSourceFactory = { CustomPageDataSource(shoeRepository) }).flow
fun setBrand(br: String) {
if (br == ALL) {
shoes = Pager(config = PagingConfig(
pageSize = 20
, enablePlaceholders = false
, initialLoadSize = 20
), pagingSourceFactory = { CustomPageDataSource(shoeRepository) }).flow
} else {
val array: Array<String> =
when (br) {
NIKE -> arrayOf("Nike", "Air Jordan")
ADIDAS -> arrayOf("Adidas")
else -> arrayOf(
"Converse", "UA"
, "ANTA"
)
}
shoes = shoeRepository.getShoesByBrand(array).createPager(20, 20).flow
}
}
//....
}
class ShoeFragment : Fragment() {
// ... 省略
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// ... 省略
onSubscribeUi(binding)
return binding.root
}
/**
* 鞋子數(shù)據(jù)更新的通知
*/
private fun onSubscribeUi(binding: ShoeFragmentBinding) {
// ... 省略
mNike.setOnClickListener {
viewModel.setBrand(ShoeModel.NIKE)
reInitSubscribe(adapter)
shoeAnimation()
}
mAdi.setOnClickListener {
viewModel.setBrand(ShoeModel.ADIDAS)
reInitSubscribe(adapter)
shoeAnimation()
}
mOther.setOnClickListener {
viewModel.setBrand(ShoeModel.OTHER)
reInitSubscribe(adapter)
shoeAnimation()
}
}
private fun reInitSubscribe(adapter: ShoeAdapter) {
job?.cancel()
job = viewModel.viewModelScope.launch(Dispatchers.IO) {
viewModel.shoes.collect() {
adapter.submitData(it)
}
}
}
private fun setViewVisible(isShow: Boolean) {
nikeGroup.visibility = if (isShow) View.VISIBLE else View.GONE
adiGroup.visibility = if (isShow) View.VISIBLE else View.GONE
otherGroup.visibility = if (isShow) View.VISIBLE else View.GONE
}
}
期間也試了 LiveData 轉(zhuǎn) Flow 的操作符,也沒起作用,可能自己研究的不深,有了解的同學(xué)可以探討一下哈~
五、總結(jié)
相比較 Paging 2 而言,Paging 3 的功能確實(shí)更加健全了,也值得去使用。
在后續(xù)的文章中,我還將和大家探討一下 Paging 3 的源碼,當(dāng)然,Paging 3 現(xiàn)在還處于 Alpha 階段,并不適用于我們的生產(chǎn)環(huán)境,所以我還打算和大家聊聊如何更好的使用 Paging 2。
