分页加载:Paging 3库集成、从网络获取分页数据、结合RemoteMediator实现网络+本地分页
分页加载这件事,说白了就是解决「数据太多,一次拉不完」的问题。我早期做项目时,遇到过列表加载到第50页直接OOM的情况——嗯,从那以后我再也不敢小看分页了。
Android官方给出的Paging 3库,是目前最成熟的方案。它不仅能帮你处理网络分页,还能无缝对接本地数据库。今天我们就来聊聊怎么把它用起来。
为什么选Paging 3?
你想想看,如果自己手写分页逻辑,要处理哪些事?
- 维护当前页码、加载状态
- 处理加载更多时的UI刷新
- 避免重复请求
- 支持列表局部刷新
- 内存管理——防止数据堆积
这些Paging 3全帮你搞定了。它基于Kotlin协程和Flow构建,跟咱们课程里讲的Retrofit + Coroutine + Flow天然契合。
核心组件一览:
- PagingSource:定义数据从哪里来(网络或数据库)
- RemoteMediator:协调网络和本地数据源,实现「网络+缓存」
- PagingData:分页数据的容器,配合Flow使用
- PagingDataAdapter:RecyclerView的适配器,自动处理DiffUtil
从网络获取分页数据:最简单的场景
我们先从纯网络分页开始。假设后端API长这样:
interface ApiService {
@GET("users")
suspend fun getUsers(
@Query("page") page: Int,
@Query("size") size: Int = 20
): Response<UserResponse>
}
data class UserResponse(
val data: List<User>,
val totalPages: Int,
val currentPage: Int
)
接下来写一个PagingSource:
class UserPagingSource(
private val api: ApiService
) : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
return try {
val page = params.key ?: 1
val response = api.getUsers(page, params.loadSize)
val users = response.body()?.data ?: emptyList()
val totalPages = response.body()?.totalPages ?: 1
LoadResult.Page(
data = users,
prevKey = if (page > 1) page - 1 else null,
nextKey = if (page < totalPages) page + 1 else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
return state.anchorPosition?.let { anchorPos ->
state.closestPageToPosition(anchorPos)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPos)?.nextKey?.minus(1)
}
}
}
这里有个坑,我踩过好几次。getRefreshKey 如果不实现,刷新时会回到第一页,用户体验很差。它的作用是告诉Paging:刷新后应该定位到哪一页。
在ViewModel里使用:
class UserViewModel(private val api: ApiService) : ViewModel() {
val users: Flow<PagingData<User>> = Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
initialLoadSize = 40
),
pagingSourceFactory = { UserPagingSource(api) }
).flow.cachedIn(viewModelScope)
}
注意 cachedIn(viewModelScope)——这个调用很关键。它会把分页数据缓存在ViewModel的作用域里,避免屏幕旋转时重新加载。
结合RemoteMediator:网络+本地分页
纯网络分页有个问题:每次下拉刷新都要重新请求第一页,而且离线时完全不能用。我个人习惯的做法是:用Room做本地缓存,RemoteMediator做网络同步。
先定义Room实体和DAO:
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: Int,
val name: String,
val email: String
)
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY id ASC")
fun getUsers(): PagingSource<Int, UserEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(users: List<UserEntity>)
@Query("DELETE FROM users")
suspend fun clearAll()
}
然后写RemoteMediator:
class UserRemoteMediator(
private val api: ApiService,
private val dao: UserDao
) : RemoteMediator<Int, UserEntity>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, UserEntity>
): MediatorResult {
return try {
val page = when (loadType) {
LoadType.REFRESH -> 1
LoadType.PREPEND -> return MediatorResult.Success(
endOfPaginationReached = true
)
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull()
if (lastItem == null) 1 else (lastItem.id / state.config.pageSize) + 1
}
}
val response = api.getUsers(page, state.config.pageSize)
val users = response.body()?.data ?: emptyList()
if (loadType == LoadType.REFRESH) {
dao.clearAll()
}
dao.insertAll(users.map { it.toEntity() })
MediatorResult.Success(
endOfPaginationReached = users.isEmpty()
)
} catch (e: Exception) {
MediatorResult.Error(e)
}
}
}
在ViewModel里组合使用:
class UserViewModel(
private val api: ApiService,
private val dao: UserDao
) : ViewModel() {
val users: Flow<PagingData<UserEntity>> = Pager(
config = PagingConfig(pageSize = 20),
remoteMediator = UserRemoteMediator(api, dao),
pagingSourceFactory = { dao.getUsers() }
).flow.cachedIn(viewModelScope)
}
我的经验:RemoteMediator的loadType判断一定要仔细。PREPEND通常直接返回成功,因为我们是单向分页(往下翻)。REFRESH时清空数据库再重新插入,保证数据一致性。
核心流程可视化
下面这张图展示了整个数据流向:
避坑指南
我曾经在RemoteMediator里犯过一个低级错误:没有处理LoadType.PREPEND,导致向上滑动时无限触发网络请求。后来加了判断才解决。
注意事项:
- RemoteMediator的
load方法运行在后台线程,不要直接更新UI - 数据库写入操作要放在事务里,避免部分写入导致数据不一致
PagingConfig的pageSize要根据后端接口调整,不是越大越好- 记得在DAO里返回
PagingSource类型,而不是Flow或LiveData
总结一下
Paging 3 + RemoteMediator这套组合拳,说白了就是「网络请求数据,本地缓存兜底」。用户在线时从网络拉取最新数据并写入Room,离线时直接从数据库读取。既保证了数据新鲜度,又实现了离线可用。
我个人建议:如果你的App需要展示长列表,并且对离线体验有要求,直接上RemoteMediator。别犹豫,它值得你花时间学习。
核心要点回顾:
- PagingSource负责定义数据来源,RemoteMediator负责协调网络和本地
- Room作为本地缓存,配合PagingSource实现离线分页
- cachedIn()避免屏幕旋转时重复加载
- getRefreshKey()保证刷新后定位正确