前言
Android 列表分頁(yè)加載組件 paging3 alpha版本已經(jīng)出來(lái)很久了。目前到了alpha7;
分享一下在項(xiàng)目中使用的經(jīng)驗(yàn)和坑;不講原理和源碼,純使用經(jīng)驗(yàn)分享!
(不要問(wèn)我為啥把a(bǔ)lpha版本用在項(xiàng)目中,問(wèn)就是任性,問(wèn)就是paging2太難用了)
準(zhǔn)備工作
1.依賴:
本文撰寫日期:2020-10-21;最新版為3.0.0-alpha07
//java
implementation 'androidx.paging:paging-runtime:3.0.0-alpha07'
//kotlin
implementation 'androidx.paging:paging-runtime-ktx:3.0.0-alpha07'
根據(jù)語(yǔ)言二選一即可,我使用的是kotlin;
使用:
1.adapter
使用paging3 ,RecyclerView的adapter 必須繼承 PagingDataAdapter
因?yàn)楹罄m(xù)分頁(yè)的UI和操作都?xì)w于 adapter 管理;
adpater 構(gòu)造必須傳參數(shù) DiffUtil.ItemCallback ;
用過(guò) AsyncListDiffer 的小伙伴應(yīng)該明白它的作用;
不明白的可以參考一下這篇文章:Android AsyncListDiffer-RecyclerView最好的伙伴
DiffUtil.ItemCallback 簡(jiǎn)單介紹:
DiffUtil.ItemCallback的作用就是取代notifyDataSetChanged粗暴刷新列表的;
畢竟粗暴刷新比較消耗性能;
主要介紹三個(gè)方法:
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {}
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {}
override fun getChangePayload(oldItem: T, newItem: T): Any? {}
paging3的設(shè)計(jì)理念是不建議對(duì)列表數(shù)據(jù)直接修改;而是對(duì)數(shù)據(jù)源進(jìn)行操作,數(shù)據(jù)源的變化會(huì)自動(dòng)更新到列表;
DiffUtil.ItemCallback 就是用來(lái)比對(duì)數(shù)據(jù)變化,從而決定更新對(duì)應(yīng)UI;并執(zhí)行條目動(dòng)畫;
areItemsTheSame
比對(duì)新舊條目是否是同一個(gè)條目;
一般比對(duì)條目的唯一標(biāo)示id即可,謹(jǐn)慎對(duì)待,如果條目不同則可能不會(huì)更新UI;areContentsTheSame
當(dāng)上面的方法確定是同一個(gè)條目之后,這里比對(duì)條目的內(nèi)容是否一樣,不一樣則會(huì)更新條目UI
建議這里的比對(duì)把UI展示的數(shù)據(jù)都寫上,寫漏了會(huì)導(dǎo)致UI不更新對(duì)應(yīng)字段;getChangePayload (可選)
這個(gè)方法對(duì)應(yīng) RcyclerView的 adapter的 第三個(gè)參數(shù);用于條目?jī)?nèi)部的局部刷新;
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payloads: MutableList<Any>
)
2.數(shù)據(jù)請(qǐng)求處理
這里利用知乎日?qǐng)?bào)的接口作為范例:
沒(méi)有使用到paging3的數(shù)據(jù)庫(kù)緩存方案 remoteMediator;因?yàn)閰?shù)被注解為
@OptIn(ExperimentalPagingApi::class)還在測(cè)試中;這里講解純網(wǎng)絡(luò)請(qǐng)求分頁(yè)方案;
實(shí)際項(xiàng)目中,不可能每個(gè)列表接口都做數(shù)據(jù)庫(kù)緩存的,工作量太大;
paging3 數(shù)據(jù)請(qǐng)求主要用到3個(gè)類:
- Pager
- PagingConfig
- PagingSource
- Pager 分頁(yè)數(shù)據(jù)的主要入口,這是它的構(gòu)造:
class Pager<Key : Any, Value : Any>
@JvmOverloads constructor(
config: PagingConfig,
initialKey: Key? = null,
@OptIn(ExperimentalPagingApi::class)
remoteMediator: RemoteMediator<Key, Value>? = null,
pagingSourceFactory: () -> PagingSource<Key, Value>
)
它的泛型
Key -> 分頁(yè)標(biāo)志 ,類似于頁(yè)碼,或者其它告訴后端我要哪一頁(yè)的參數(shù);
Value -> 列表數(shù)據(jù)的單個(gè)數(shù)據(jù)類型,就是每個(gè)條目的類型;
參數(shù)解釋:
config :分頁(yè)配置,見下面介紹
initialKey : 初始頁(yè)的頁(yè)碼 (可選)
remoteMediator :遠(yuǎn)程數(shù)據(jù)解調(diào)員;網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)后處理的類,可以做數(shù)據(jù)緩存
pagingSourceFactory:數(shù)據(jù)源工廠(每次刷新數(shù)據(jù)都會(huì)生產(chǎn)新的數(shù)據(jù)源)
- PagingConfig 介紹
Pager第一個(gè)參數(shù):config: PagingConfig 分頁(yè)邏輯:每頁(yè)多少條之類的設(shè)置;
構(gòu)造:
class PagingConfig @JvmOverloads constructor(
val pageSize: Int,
@IntRange(from = 0)
val prefetchDistance: Int = pageSize,
val enablePlaceholders: Boolean = true,
@IntRange(from = 1)
val initialLoadSize: Int = pageSize*DEFAULT_INITIAL_PAGE_MULTIPLIER,
val maxSize: Int = MAX_SIZE_UNBOUNDED,
val jumpThreshold: Int = COUNT_UNDEFINED
)
參數(shù)解釋:
pageSize:每頁(yè)多少個(gè)條目;必填
prefetchDistance :預(yù)加載下一頁(yè)的距離,滑動(dòng)到倒數(shù)第幾個(gè)條目就加載下一頁(yè),無(wú)縫加載(可選)默認(rèn)值是pageSize
enablePlaceholders:是否啟用條目占位,當(dāng)條目總數(shù)量確定的時(shí)候;列表一次性展示所有條目,但是沒(méi)有數(shù)據(jù);在adapter的onBindViewHolder里面綁定數(shù)據(jù)時(shí)候,是空數(shù)據(jù),判斷是空數(shù)據(jù)展示對(duì)應(yīng)的占位item;可選,默認(rèn)開啟。
initialLoadSize :第一頁(yè)加載條目數(shù)量 ,可選,默認(rèn)值是 3*pageSize (有時(shí)候需要第一頁(yè)多點(diǎn)數(shù)據(jù)可用)
maxSize :定義列表最大數(shù)量;可選,默認(rèn)值是:Int.MAX_VALUE
jumpThreshold:暫時(shí)還不知道用法,從文檔注釋上看,是滾動(dòng)大距離導(dǎo)致加載失效的閾值;可選,默認(rèn)值是:Int.MIN_VALUE (表示禁用此功能)
- PagingSource 分頁(yè)數(shù)據(jù)源
pagingSourceFactory 工廠生產(chǎn)的產(chǎn)品;
abstract class PagingSource<Key : Any, Value : Any> {
abstract suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value>
}
泛型同 Pager 泛型,要實(shí)現(xiàn)的主要方法就一個(gè):比paging2方便了不知道多少倍
參數(shù)解釋:
params :請(qǐng)求列表需要的參數(shù)
返回值:
LoadResult :列表數(shù)據(jù)請(qǐng)求結(jié)果,包含下一頁(yè)要請(qǐng)求的key
用法范例:
val allNews = Pager(PagingConfig(20), initialKey = initialKey) {
object : PagingSource<Long, News.StoriesBean>() {
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, News.StoriesBean> {
val date = params.key ?: initialKey
return try {
val data = api.getNews(date).await() //網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)
LoadResult.Page(data.stories, null, data.date.toLong())
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
}
.flow
.cachedIn(viewModelScope)
.asLiveData(viewModelScope.coroutineContext)
LoadResult.Page 解釋:
constructor(
data: List<Value>,
prevKey: Key?,
nextKey: Key?
)
參數(shù):
data :返回的數(shù)據(jù)列表
prevKey :上一頁(yè)的key (傳 null 表示沒(méi)有上一頁(yè))
nextKey :下一頁(yè)的key (傳 null 表示沒(méi)有下一頁(yè))
paging3 使用 flow 傳遞數(shù)據(jù),不了解的可以搜索一下flow
cachedIn 綁定協(xié)程生命周期,必須加上,否則可能崩潰
asLiveData 熟悉livedata的都知道怎么用
綁定數(shù)據(jù)給adapter
model.allNews.observe(this@ZhiHuActivity, Observer {
lifecycleScope.launchWhenCreated {
adapter.submitData(it)
}
})
adapter.submitData 是一個(gè)協(xié)程掛起(suspend)操作,所以要放入?yún)f(xié)程賦值
lifecycleScope.launchWhenCreated 和 viewModelScope
需要依賴協(xié)程的生命周期輔助,見下面:
//生命周期輔助ktx
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-beta01'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-beta01'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-beta01'
3.UI狀態(tài)處理和操作
下拉刷新
第一次請(qǐng)求不需要任何操作,訂閱數(shù)據(jù)直接請(qǐng)求;
手動(dòng)下拉刷新直接調(diào)用:
adapter.refresh()
就是這么簡(jiǎn)單,比paging2方便多了
上拉加載
paging3是無(wú)縫加載,實(shí)際沒(méi)有手動(dòng)上拉的操作
但是用戶滑動(dòng)過(guò)快的話還是會(huì)展示上拉的UI,下面會(huì)有UI的處理邏輯
失敗重試
adapter.retry()
主要用于加載更多的重試。
UI狀態(tài)處理
adapter.addLoadStateListener :添加狀態(tài)監(jiān)聽:
adapter.addLoadStateListener {
when (it.refresh) {
is LoadState.Loading -> {}
is LoadState.NotLoading -> {}
is LoadState.Error -> {}
}
}
狀態(tài)返回的參數(shù) CombinedLoadStates,包含了
refresh,prepend,append,source,mediator 五種行為的狀態(tài)
分別是:
刷新,向前加載更多,向后加載更多,數(shù)據(jù)源,調(diào)解員
每個(gè)行為分為3中狀態(tài):
- LoadState.Loading 加載中 (加載數(shù)據(jù)時(shí)候回調(diào))
- LoadState.NotLoading 沒(méi)有加載中 (加載數(shù)據(jù)前和加載數(shù)據(jù)完成后回調(diào))
- LoadState.Error 加載失敗 (加載數(shù)據(jù)失敗回調(diào))
我們一般業(yè)務(wù)只關(guān)注 刷新和向后加載更多;
以SmartRefreshLayout為例:
下拉刷新狀態(tài)處理:
//因?yàn)樗⑿虑耙矔?huì)調(diào)用LoadState.NotLoading,所以用一個(gè)外部變量判斷是否是刷新后
var hasRefreshing = false
adapter.addLoadStateListener {
when (it.refresh) {
is LoadState.Loading -> {
hasRefreshing = true
//如果是手動(dòng)下拉刷新,則不展示loading頁(yè)
if (srl_refresh.state != RefreshState.Refreshing) {
statePager.showLoading()
}
}
is LoadState.NotLoading -> {
if (hasRefreshing) {
hasRefreshing= false
statePager.showContent()
srl_refresh.finishRefresh(true)
//如果第一頁(yè)數(shù)據(jù)就沒(méi)有更多了,第一頁(yè)不會(huì)觸發(fā)append
if (it.source.append.endOfPaginationReached){
//沒(méi)有更多了(只能用source的append)
srl_refresh.finishLoadMoreWithNoMoreData()
}
}
}
is LoadState.Error -> {
statePager.showError()
srl_refresh.finishRefresh(false)
}
}
}
上拉加載更多狀態(tài)處理:
//因?yàn)樗⑿虑耙矔?huì)調(diào)用LoadState.NotLoading,所以用一個(gè)外部變量判斷是否是加載更多后
var hasLoadingMore = false
adapter.addLoadStateListener {
when (it.append) {
is LoadState.Loading -> {
hasLoadingMore = true
//重置上拉加載狀態(tài),顯示加載loading
srl_refresh.resetNoMoreData()
}
is LoadState.NotLoading -> {
if (hasLoadingMore) {
hasLoadingMore = false
if (it.source.append.endOfPaginationReached){
//沒(méi)有更多了(只能用source的append)
srl_refresh.finishLoadMoreWithNoMoreData()
}else{
srl_refresh.finishLoadMore(true)
}
}
}
is LoadState.Error -> {
srl_refresh.finishLoadMore(false)
}
}
}
上面代碼就是刷新和加載更多狀態(tài)監(jiān)聽了,有一個(gè)問(wèn)題:
第一頁(yè)數(shù)據(jù)如果沒(méi)有更多了,是不會(huì)觸發(fā) append 的 LoadState.Loading 狀態(tài),所以得在refresh里面判斷一下;
刷新失敗處理:
直接調(diào)用刷新即可
adapter.refresh()
加載更多失敗處理:
srl_refresh.setOnLoadMoreListener {
adapter.retry()
}
為什么是重試?
因?yàn)閜aging是無(wú)縫加載,所以沒(méi)有手動(dòng)上拉加載邏輯
retry()雖然是重試,但是paging已處理,只有失敗后會(huì)重試,所以這里上拉加載調(diào)用重試沒(méi)問(wèn)題
關(guān)于Header和 Footer
PagingDataAdapter 是支持 添加Header和Footer 的
adapter.withLoadStateHeader(header: LoadStateAdapter<*>)
adapter.withLoadStateFooter(header: LoadStateAdapter<*>)
adapter.withLoadStateHeaderAndFooter(header: LoadStateAdapter<*>,
footer: LoadStateAdapter<*>)
LoadStateAdapter : 也是一個(gè) RecyclerView.Adapter ;
類似于多條目布局,只是分成多個(gè)adapter
谷歌出過(guò)一個(gè) MergeAdapter,就是把多個(gè)RecyclerView.Adapter 合并成一個(gè),
有興趣的小伙伴可以搜索一下。這里就不介紹了;