paging3 ǀ 官方分页库拆解与应用(下)
一 前言
未熟悉 Paging3 的可先查看上一篇文章:paging3 ǀ 官方分页库拆解与应用(上)
本文demo已放到git仓库
本篇主要讲述两大功能:
- 1. 状态管理:在正常的业务开发里,完善的界面是有状态的,loading -> success/error -> retry -> refresh。每一个状态对应不同ui展示。paging3是支持状态控制和监听的。
- 2. 本地数据库和网络数据结合:paging3提供了
remoteMediator(实验性api)
和Room扩展类,能快速支持开发者从本地数据库和网络加载分页数据
二 状态管理
代码 -> LoadStateFragment
2.1 请求状态分类
请求分为三种:
- 刷新:
LoadStates.refresh
/LoadType.REFRESH
- 加载下一页:
LoadStates.append
/LoadType.PREPEND
- 加载上一页:
LoadStates.prepend
/LoadType.PREPEND
每种请求的结果由LoadState
表示:
- 加载中:
LoadState.Loading
- 加载完成:
LoadState.NotLoading(endOfPaginationReached)
endOfPaginationReached:表示该请求类型数据是否全部请求完毕,如append 请求的话,true 表示已经是最后一页数据了,没有更多了。 - 加载失败:
LoadState.Error
2.2 loading -> error -> retry
- 1、 在 pagingSource的load中,第一次请求时模拟返回异常
- 2、 监听 PagingAdapter的状态 flow
//step2 监听 loadStateFlow
lifecycleScope.launch{
adapter.loadStateFlow.collect{combinedLoadStates ->
binding.loading.isVisible = combinedLoadStates.refresh is LoadState.Loading
binding.retry.isVisible = combinedLoadStates.refresh is LoadState.Error
binding.rv.isVisible = combinedLoadStates.refresh is LoadState.NotLoading
}
}
combinedLoadStates分得很细,按一开始讲的,记录三种请求refresh,append,prepend的状态结果
同时它还含有 source 和 mediator 的loadStates用于标记不同数据源的请求状态:
- source 代表的是来自 pagingSouce 数据源的 loadStates;
- mediator 代表的是后续加入的 remoteMediator 数据源加载状态,后续会讲remoteMediator。
2.3 加载下一页/append状态管理:loading -> error -> retry
加载上一页/prepend的状态管理 ,写法一样就不重复了
paging3 也开发了 api 支持该功能:
- 3、 定义转用于状态管理的 footer 继承LoadStateAdapter:
class LoadStateFooterAdapter(private val retry: () -> Unit) : LoadStateAdapter<LoadStateFooterViewHolder>() {
override fun onBindViewHolder(holder: LoadStateFooterViewHolder, loadState: LoadState) {
holder.bind(loadState)
}
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateFooterViewHolder {
return LoadStateFooterViewHolder.create(parent, retry)
}
}
viewHolder 里面是自定义的状态管理view。需要注意的是,这里的 onbindView 获得的 data是LoadState!!!
- 4、 根据
LoadState
渲染 item
fun bind(loadState: LoadState) {
//step 4 根据 LoadState 渲染 item
if (loadState is LoadState.Error) {
binding.errorMsg.text = loadState.error.localizedMessage
}
binding.loading.isVisible = loadState is LoadState.Loading
binding.retry.isVisible = loadState is LoadState.Error
binding.errorMsg.isVisible = loadState is LoadState.Error
}
- 5、 将状态 adapter 跟 pagingAdapter 绑定
val adapter = LoadStateAdapter()
//step5 将 stateFooter 与 adapter 绑定
binding.rv.adapter = adapter.withLoadStateFooter(LoadStateFooterAdapter { adapter.retry() })
还有
withLoadStateHeaderAndFooter
&withLoadStateHeader
可供选择
- 6、 使得第一次 loadMore 异常
//step6 第一次 loadMore 返回异常
if (key != 0 && emitLoadMoreError){
delay(1000)
emitLoadMoreError = false
return LoadResult.Error(IllegalStateException("错啦~~"))
}
这就完成了状态管理了。
看其源码,也是监听 loadStates,并在loadState发生变化时notifyItem
注意:这里有个问题,向下加载失败后,再次滚动到底部,他是不会自动重试的!!!需要手动调用 pagingAdapter.retry
三 从网络和数据库加载分页数据:remoteMediator
代码 -> RemoteMediatorFragment
RemoteMediator
是 paging 提供的组件,快速实现从网络端和本地数据库获取分页数据。
Room 本身已经提供了 paging 快速支持,只需将返回值指定为PagingSource<key,value>
即可,如:
当首次加载数据库或数据库无更多数据时(也是分为 refresh、append,prepend
),都会触发 remoteMediator 向网络端请求数据。
3.1 通过 Room,创建简易db&dao
Entiry & DAO
@Dao
interface MessageDao {
//ORDER BY id ASC
@Query("SELECT * FROM Message")
fun getAllMessage(): PagingSource<Int, Message>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(msgs: List<Message>)
}
@Entity
data class Message(
@PrimaryKey
val id: Int,
val content: String,
)
3.2 定义 remoteMediator
3.2.1 继承RemoteMediator<key,value>
:
@OptIn(ExperimentalPagingApi::class)
class MyRemoteMediator(
private val service: FakeService,
private val database:MessageDb
):RemoteMediator<Int,Message>(){
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Message>
): MediatorResult {
val key = when(loadType){
LoadType.REFRESH -> {
}
LoadType.APPEND -> {
}
LoadType.PREPEND -> {
}
}
......
}
同样的也是分为三种类型,refresh,append,prepend。
refresh
:是在首次加载本地数据时立即触发,也可通过重写方法,不触发 refresh
override suspend fun initialize(): InitializeAction {
//skip 则第一次加载数据时不触发刷新
return InitializeAction.SKIP_INITIAL_REFRESH
}
append
:数据库没有更多的数据显示上一页时,触发remoteMediator 查询服务端是否有更多上一页数据;prepend
:数据库没有更多的数据显示 下一页时,触发remoteMediator 查询服务端是否有更多下一页数据;
load 方法加载饭后两种结果:
MediatorResult.Success
:网络数据成功则自行存入数据库并返回MediatorResult.Success(endOfPaginationReached)
,endOfPaginationReached表示是否是最后的 item,即 true 表示没有更多数据);MediatorResult.error(throwable)
:失败
3.2.2 请求数据并更新数据库
1、获取请求 key:
- refresh 情况下,为了界面不闪烁滚动,一般是根据当前 recyclerview的可见的 viewHolder 对应的data 作为 刷新 key。
private fun getKeyClosestToCurrentPosition(state: PagingState<Int, Message>):Int?{
return state.anchorPosition?.let {anchorPosition ->
state.closestItemToPosition(anchorPosition)?.id?.plus(1)
}
}
state: PagingState记录当前所有 paging 数据和状态,anchorPosition则是它自行算出的最近滚动后可见的 viewholder 位置。所以我们根据这个 position 获取 closestItemToPosition作为 key 去刷新界面。
- prepend 情况下,是为了加载下一页,所以直接拿当前已获得的数据的最后一页最后一个 item 作为请求 key
private fun getKeyForLastItem(state: PagingState<Int, Message>): Int? {
return state.pages.lastOrNull() { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { msg ->
msg.id + 1
}
}
2、根据 key请求数据并返回 success/error
return try {
val newData = service.fetchData(key.until(key + 10))
database.messageDao().insert(newData)
MediatorResult.Success(endOfPaginationReached = false)
} catch (exception: IOException) {
MediatorResult.Error(exception)
}
注意,这里的 remotemedia 是只负责拉取网络端数据并保存到本地数据库,随后room 扩展的 pagingSource 会自动刷新数据到 ui。
3.3 将 remoteMediator 与数据库的 pagingSource 绑定
将 pagingSourceFactory 改为 room 返回的 pagingsource,并添加 remoteMediator 即可:
Pager(
config = PagingConfig(pageSize = 10),
pagingSourceFactory = { MessageDb.get(application.applicationContext).messageDao().getAllMessage() },
remoteMediator = MyRemoteMediator(FakeService(), MessageDb.get(application.applicationContext))
)
ok ,这就完成了本地数据和网络数据的分页加载。remoteMediator 此时还是实验性 api,使用到的地方需要加@OptIn(ExperimentalPagingApi::class)
,后续api 也可能会变动。
3.4 闲谈
paging 对数据源严格把控,提倡开发者通过改变 layoutManager 或其他方式来适配需求。比如常见的聊天界面是倒序的,而且是向上看看有没有更多的旧聊天信息,这点的话可以在 sql 语句加句 order by xxx DESC/ASC
,并将 LinearLayoutManager
的reverseLayout
设为 true。
本文demo已放到git仓库
四 ❤️ 感谢
如果觉得这篇内容对你有所帮忙,一键三连支持下(👍)
关于纠错和建议:欢迎直接在留言分享记录(🌹)