深度解析:如何用 Paging3 优雅地处理复杂数据源切换

169 阅读4分钟

在项目中,当需要对在线数据和本地数据进行混合分页处理时,Paging3 提供了强大的工具,能够帮助我们优雅地实现数据加载和切换逻辑。然而,对于一些复杂的场景,例如在线数据与本地数据的动态切换,或者在没有在线数据时优雅降级到本地数据,这些仍然需要我们做一些额外的处理和优化。

图片

一、背景与需求

我们的项目需要实现一个复杂的分页逻辑:

  1. 在线与本地数据的混合排序:当有在线数据时,在线数据与本地数据按时间排序混合展示。
  2. 无在线数据时降级为本地数据:如果在线数据为空或加载失败,展示本地存储的数据。
  3. 刷新和切换逻辑:支持在不同状态下自由切换在线与本地数据源。

二、基础实现步骤

1. 实现 PagingSource

PagingSource 是 Paging3 的核心组件,用于处理分页加载逻辑。我们需要实现一个通用的数据源,在加载在线数据失败时切换到本地数据源。

2. 在线与本地数据切换逻辑

在 PagingSource 中,数据请求失败时,查询本地数据并返回。

3. 数据源切换

当用户刷新或需要重新加载数据时,动态切换在线和本地数据源。

以下是具体代码实现示例:

class ListPagingSource(private val token: String) : PagingSource<Int, Record>() {

    private val mRecordDao by lazy { AppDataBase.getInstance().getRecordDao() }

    override fun getRefreshKey(state: PagingState<Int, Record>): Int? {
        // 定义刷新关键点
        return null
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Record> {
        return try {
            if (token.isEmpty()) {
                throw IllegalArgumentException("Token 不能为空")
            }

            val page = params.key ?: FIRST_PAGE_INDEX
            val response = Service.getPage(token, page) // 请求在线数据
            val responseData = response.data

            if (response.isSuccess && responseData != null) {
                // 返回在线数据
                LoadResult.Page(
                    data = responseData.records,
                    prevKey = if (page > FIRST_PAGE_INDEX) page - 1 else null,
                    nextKey = if (page < responseData.totalPage) page + 1 else null
                )
            } else {
                // 切换到本地数据
                val localData = mRecordDao.getAllRecord()
                LoadResult.Page(data = localData, prevKey = null, nextKey = null)
            }
        } catch (e: Exception) {
            // 错误处理
            LoadResult.Error(e)
        }
    }

    companion object {
        private const val FIRST_PAGE_INDEX = 1
    }
}

三、本地数据不分页的切换方案

如果本地数据量较小或者业务场景不复杂,可以不对本地数据进行分页处理,而是直接在加载在线数据失败时展示全部本地数据。此方案实现简单,适合轻量级场景,但当数据量较大时,用户体验可能会受到影响。

实现示例:

if (response.isSuccess) {
    // 展示在线数据
    LoadResult.Page(data = responseData.records, prevKey = prevKey, nextKey = nextKey)
} else {
    // 加载全部本地数据
    val localData = mRecordDao.getAllRecord()
    LoadResult.Page(data = localData, prevKey = null, nextKey = null)
}

优点:实现简单,适合数据量较小的场景。
缺点:数据量大时,加载速度较慢且可能引发性能问题。

四、本地数据分页的切换方案

为了提升大数据量场景下的用户体验,我们可以对本地数据和在线数据分别进行分页处理。以下是方案步骤:

  1. 为本地数据实现单独的 PagingSource
  2. 在线数据和本地数据独立运行,并支持动态切换。
  3. 解决切换数据源时的冲突问题。

实现独立的本地数据 PagingSource

fun getLocalPagedData(): Flow<PagingData<Record>> {
    return Pager(
        config = PagingConfig(
            pageSize = 30,
            enablePlaceholders = false
        ),
        pagingSourceFactory = { dao.getPagedRecords() }
    ).flow.cachedIn(viewModelScope)
}

切换数据源时避免崩溃

Paging3 在加载数据时直接切换数据源会导致 IndexOutOfBoundsException,解决方法是在切换数据源前先提交一个空的 PagingData

cloudPageAdapter.submitData(PagingData.empty()) // 提交空数据
cloudPageAdapter.submitData(newPagingData)     // 提交新数据源

进一步优化:处理用户点击事件崩溃

如果用户在空数据加载时触发点击事件,也可能导致崩溃。通过延迟加载新数据,可以有效避免这一问题:

launch(Dispatchers.IO) {
    cloudPageAdapter.submitData(PagingData.empty())
    delay(100) // 延迟 100 毫秒
    cloudPageAdapter.submitData(newPagingData)
}

五、总结

Paging3 提供了强大的分页加载能力,但在复杂的场景中,例如在线与本地数据切换时,仍需要我们精心设计和优化。以下是关键点:

  1. 动态切换数据源时,确保 Paging3 的数据状态一致。
  2. 本地数据量较小时,可以不分页;但大数据量场景下分页更高效。
  3. 避免用户在数据切换时操作导致的崩溃问题。

原文地址:mp.weixin.qq.com/s/nYINi-t_F…