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 引擎 -> UI 展示。中间用 Flow 串联,每一层都可以独立测试、独立替换。
总结一下
Paging 3 和 Flow 的结合,让分页加载变得前所未有的简单。我个人觉得,最大的价值在于:
- 声明式编程:你只需要描述「数据从哪里来」,不用关心「什么时候加载」
- 生命周期安全:cachedIn() 自动处理配置变更,不会重复请求
- 可测试性:PagingSource 和 RemoteMediator 都是纯接口,单元测试很好写
嗯,最后说一句:别把 Paging 3 想得太复杂。它本质上就是一个「会自己翻页的 Flow」。你只要把数据源写好,剩下的交给框架就行。