16、Flow与Paging3:Paging3的Flow支持、分页数据的Flow化、RemoteMediator的Flow实现

分页加载,说白了就是「一次别拉太多,分批给用户看」。Android 开发这么多年,从 CursorLoader 到 Paging 2,再到现在的 Paging 3,我算是看着分页方案一步步进化过来的。Paging 3 最大的变化是什么?就是全面拥抱了 Kotlin Flow。

嗯,今天我们就来聊聊,怎么用 Flow 把分页这件事做得更优雅。

Paging3 的 Flow 支持

Paging 3 从设计之初就把 Flow 作为一等公民。你想想看,分页数据天然就是流式的——用户往下滑,数据源源不断产生,这不就是 Flow 的典型场景吗?

核心 API 就两个:

  • Pager:分页引擎,负责驱动数据加载
  • PagingData:分页数据的容器,本质是一个 Flow

我刚开始用 Paging 3 时,最直观的感受就是:终于不用手动管理加载状态了。以前用 Paging 2,你得自己写 DiffUtil、自己处理加载回调,现在全交给 Flow 搞定。

来看一个最基本的例子:

// 定义数据源
class MyPagingSource(
    private val api: MyApi,
    private val query: String
) : PagingSource<Int, Item>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> {
        return try {
            val page = params.key ?: 1
            val response = api.search(query, page, params.loadSize)
            LoadResult.Page(
                data = response.items,
                prevKey = if (page > 1) page - 1 else null,
                nextKey = if (response.items.isNotEmpty()) page + 1 else null
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, Item>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }
}

// 在 ViewModel 中使用
class MyViewModel : ViewModel() {
    val pagingDataFlow: Flow<PagingData<Item>> = Pager(
        config = PagingConfig(
            pageSize = 20,
            enablePlaceholders = false,
            initialLoadSize = 40
        ),
        pagingSourceFactory = { MyPagingSource(api, query) }
    ).flow
}

你看,核心逻辑就三行。Pager 返回的是一个 Flow<PagingData<T>>,直接在 ViewModel 里暴露给 UI 层就行。

关键点:PagingConfig 的 pageSize 不是硬性限制,而是「建议值」。Paging 3 会根据列表滚动速度动态调整预取数量。我遇到过有人把 pageSize 设成 100,结果内存直接爆了——别这么干,20-30 是比较合理的范围。

分页数据的 Flow 化

把分页数据变成 Flow 之后,好处就来了。你可以用 Flow 的各种操作符对分页数据进行变换、组合、过滤。

举个例子,假设我们需要对搜索结果做去重:

val deduplicatedFlow: Flow<PagingData<Item>> = pagingDataFlow
    .map { pagingData ->
        pagingData.filter { item ->
            // 过滤掉已删除的条目
            !item.isDeleted
        }
    }
    .cachedIn(viewModelScope)

这里有个坑,我踩过。PagingData 不是普通的集合,你不能直接对它做 map 操作。正确的做法是用 PagingData 提供的 transform 方法:

val transformedFlow = pagingDataFlow.map { pagingData ->
    pagingData.map { item ->
        item.copy(displayName = item.name.uppercase())
    }
}

嗯,这里要注意:cachedIn() 必须加。不加的话,每次配置变更(比如屏幕旋转)都会重新触发网络请求。我刚开始用 Paging 3 时就忘了加,结果测试说「为什么我转一下屏幕数据就刷新了?」——尴尬。

个人习惯:我会把 cachedIn 放在 ViewModel 里,而不是 Fragment 或 Activity 里。这样即使 UI 重建,数据流也不会中断。

RemoteMediator 的 Flow 实现

前面说的 PagingSource 只处理单一数据源。但实际项目中,我们通常需要「网络 + 本地缓存」的混合模式。这时候就要用到 RemoteMediator

RemoteMediator 的作用是什么?说白了就是「协调员」——它决定什么时候从网络加载,什么时候从本地读取,以及如何把网络数据写入本地。

来看一个完整的 RemoteMediator 实现:

class MyRemoteMediator(
    private val api: MyApi,
    private val db: MyDatabase
) : RemoteMediator<Int, Item>() {

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, Item>
    ): MediatorResult {
        return try {
            // 1. 确定要加载的页码
            val page = when (loadType) {
                LoadType.REFRESH -> null
                LoadType.PREPEND -> return MediatorResult.Success(
                    endOfPaginationReached = true
                )
                LoadType.APPEND -> {
                    val lastItem = state.lastItemOrNull()
                    if (lastItem == null) 1 else lastItem.page + 1
                }
            } ?: 1

            // 2. 从网络加载
            val response = api.getItems(page, state.config.pageSize)

            // 3. 写入本地数据库
            db.withTransaction {
                if (loadType == LoadType.REFRESH) {
                    db.dao().clearAll()
                }
                db.dao().insertAll(response.items)
            }

            // 4. 返回结果
            MediatorResult.Success(
                endOfPaginationReached = response.items.isEmpty()
            )
        } catch (e: Exception) {
            MediatorResult.Error(e)
        }
    }
}

然后把它和 PagingSource 组合起来:

val pagingDataFlow = Pager(
    config = PagingConfig(pageSize = 20),
    remoteMediator = MyRemoteMediator(api, db),
    pagingSourceFactory = { db.dao().getPagingSource() }
).flow

这里有个设计上的细节:PagingSource 只负责从本地数据库读取,RemoteMediator 负责从网络拉取并写入本地。两者各司其职,互不干扰。

我曾经踩过的坑:RemoteMediator 的 load 方法是在后台线程执行的,但数据库操作必须在同一个事务中。如果你用 Room,记得加上 withTransaction。我见过有人把 clearAll 和 insertAll 分开写,结果网络请求失败后数据库被清空了——数据全丢了。

Flow 化的 RemoteMediator 进阶

有时候我们需要在 RemoteMediator 中执行更复杂的逻辑,比如根据网络状态决定是否刷新。这时候可以用 Flow 来包装 RemoteMediator 的触发条件。

举个例子,假设我们只在 Wi-Fi 环境下才自动刷新:

class NetworkAwareMediator(
    private val api: MyApi,
    private val db: MyDatabase,
    private val connectivityManager: ConnectivityManager
) : RemoteMediator<Int, Item>() {

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, Item>
    ): MediatorResult {
        // 只在 REFRESH 时检查网络
        if (loadType == LoadType.REFRESH) {
            val isWifi = connectivityManager
                .getNetworkCapabilities(connectivityManager.activeNetwork)
                ?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true

            if (!isWifi) {
                // 非 Wi-Fi 环境,跳过网络加载
                return MediatorResult.Success(
                    endOfPaginationReached = false
                )
            }
        }

        // 正常加载逻辑...
    }
}

你想想看,这种场景用传统的回调方式实现有多麻烦?但用 RemoteMediator + Flow,逻辑就清晰多了。

知识体系总览

下面这张图总结了 Paging 3 与 Flow 结合的核心架构:

Paging 3 + Flow 核心架构 网络数据源 Retrofit / Ktor 本地数据源 Room / SQLite 缓存策略 RemoteMediator PagingSource + RemoteMediator 协调网络与本地数据加载 Flow<PagingData<T>> cachedIn() · map() · filter() · combine() RecyclerView + PagingDataAdapter

整个流程其实就三步:数据源 -> Paging 引擎 -> UI 展示。中间用 Flow 串联,每一层都可以独立测试、独立替换。

总结一下

Paging 3 和 Flow 的结合,让分页加载变得前所未有的简单。我个人觉得,最大的价值在于:

  • 声明式编程:你只需要描述「数据从哪里来」,不用关心「什么时候加载」
  • 生命周期安全:cachedIn() 自动处理配置变更,不会重复请求
  • 可测试性:PagingSource 和 RemoteMediator 都是纯接口,单元测试很好写

嗯,最后说一句:别把 Paging 3 想得太复杂。它本质上就是一个「会自己翻页的 Flow」。你只要把数据源写好,剩下的交给框架就行。

公众号:蓝海资料掘金营,微信deep3321