paging3 官方分页库拆解与使用(下)

一 前言

未了解 Paging3 的可先查看上一篇文章:paging3 官方分页库拆解与使用(上)

本文demo已放到git库房

本篇首要叙述两大功用:

  1. 1. 状况办理:在正常的事务开发里,完善的界面是有状况的,loading -> success/error -> retry -> refresh。每一个状况对应不同ui展现。paging3是支撑状况控制和监听的。

paging3  官方分页库拆解与应用(下)

paging3  官方分页库拆解与应用(下)

  1. 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

paging3  官方分页库拆解与应用(下)

  • 1、 在 pagingSource的load中,第一次恳求时模拟回来反常

paging3  官方分页库拆解与应用(下)

  • 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的状况成果

paging3  官方分页库拆解与应用(下)

一起它还含有 source 和 mediator 的loadStates用于符号不同数据源的恳求状况:

  • source 代表的是来自 pagingSouce 数据源的 loadStates
  • mediator 代表的是后续加入的 remoteMediator 数据源加载状况,后续会讲remoteMediator。

2.3 加载下一页/append状况办理:loading -> error -> retry

paging3  官方分页库拆解与应用(下)

加载上一页/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

paging3  官方分页库拆解与应用(下)

paging3  官方分页库拆解与应用(下)

留意:这儿有个问题,向下加载失利后,再次翻滚到底部,他是不会主动重试的!!!需求手动调用 pagingAdapter.retry

三 从网络和数据库加载分页数据:remoteMediator

代码 -> RemoteMediatorFragment

RemoteMediator是 paging 供给的组件,快速实现从网络端和本地数据库获取分页数据。 Room 自身已经供给了 paging 快速支撑,只需将回来值指定为PagingSource<key,value>即可,如:

paging3  官方分页库拆解与应用(下)
当初次加载数据库或数据库无更多数据时(也是分为 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,并将 LinearLayoutManagerreverseLayout设为 true。

paging3  官方分页库拆解与应用(下)

本文demo已放到git库房

四 ❤️ 感谢

假如觉得这篇内容对你有所帮助,一键三连支撑下()

关于纠错和建议:欢迎直接在留言共享记载()