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

1,363 阅读5分钟

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

一 前言

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

本文demo已放到git仓库

本篇主要讲述两大功能:

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

device-2023-06-01-101647.gif

device-2023-06-01-102127.gif

  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

device-2023-06-06-170233.gif

  • 1、 在 pagingSource的load中,第一次请求时模拟返回异常

Clipboard_2023-06-06-17-24-13.png

  • 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的状态结果

Clipboard_2023-06-06-17-42-50.png

同时它还含有 source 和 mediator 的loadStates用于标记不同数据源的请求状态:

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

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

device-2023-06-07-104839.gif

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

Clipboard_2023-06-07-11-47-15.png

Clipboard_2023-06-07-11-48-43.png

注意:这里有个问题,向下加载失败后,再次滚动到底部,他是不会自动重试的!!!需要手动调用 pagingAdapter.retry

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

代码 -> RemoteMediatorFragment

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

Clipboard_2023-06-07-14-25-43.png 当首次加载数据库或数据库无更多数据时(也是分为 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。

device-2023-06-08-145813.gif

本文demo已放到git仓库

四 ❤️ 感谢

如果觉得这篇内容对你有所帮忙,一键三连支持下(👍)

关于纠错和建议:欢迎直接在留言分享记录(🌹)