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库房
四 ❤️ 感谢
假如觉得这篇内容对你有所帮助,一键三连支撑下()
关于纠错和建议:欢迎直接在留言共享记载()