文章目录
- [一. 面试题](#一. 面试题)
-
-
- [1. Paging3 的核心组件有哪些?各自职责是什么?](#1. Paging3 的核心组件有哪些?各自职责是什么?)
- [2. Paging3 相比 Paging2 有哪些核心改进?](#2. Paging3 相比 Paging2 有哪些核心改进?)
- [3. PagingSource 和 RemoteMediator 的区别与适用场景?**](#3. PagingSource 和 RemoteMediator 的区别与适用场景?**)
- [4. Paging3 + Room 实现离线分页的完整流程是怎样的? **](#4. Paging3 + Room 实现离线分页的完整流程是怎样的? **)
- [5. Paging3 的预取(Prefetch)机制是怎样的?如何配置?](#5. Paging3 的预取(Prefetch)机制是怎样的?如何配置?)
- [6. PagingDataAdapter 为什么用 DiffUtil?和普通 Adapter 有什么区别?](#6. PagingDataAdapter 为什么用 DiffUtil?和普通 Adapter 有什么区别?)
- [7. Paging3 如何处理加载状态与错误重试?](#7. Paging3 如何处理加载状态与错误重试?)
- [8. Paging3 使用中有哪些常见的坑与注意事项?](#8. Paging3 使用中有哪些常见的坑与注意事项?)
- [9. pagingSourceFactory 和 remoteMediator 如何理解这俩参数](#9. pagingSourceFactory 和 remoteMediator 如何理解这俩参数)
-
- [二. 代码demo](#二. 代码demo)
-
-
-
- [1. 数据实体类](#1. 数据实体类)
- [2. 分页数据源:PagingSource 实现](#2. 分页数据源:PagingSource 实现)
- [3. 列表适配器:PagingDataAdapter 实现](#3. 列表适配器:PagingDataAdapter 实现)
- [4. ViewModel 层:Pager 配置与数据流](#4. ViewModel 层:Pager 配置与数据流)
- [5. UI 层:Activity 中使用](#5. UI 层:Activity 中使用)
-
-
- [三. Room + RemoteMediator 标准 Demo(离线优先分页架构)](#三. Room + RemoteMediator 标准 Demo(离线优先分页架构))
-
-
-
- [1. 数据库实体类](#1. 数据库实体类)
- [2. DAO 数据访问接口](#2. DAO 数据访问接口)
- [3. Room 数据库类](#3. Room 数据库类)
- [4. 模拟网络请求接口](#4. 模拟网络请求接口)
- [5. 核心:RemoteMediator 实现](#5. 核心:RemoteMediator 实现)
- [6. ViewModel 层](#6. ViewModel 层)
- [7. UI 层(与纯网络版完全一致)](#7. UI 层(与纯网络版完全一致))
- 架构核心总结(面试可直接说)
-
-
一. 面试题
1. Paging3 的核心组件有哪些?各自职责是什么?
答案:
- PagingSource:分页数据源,定义单页数据的加载逻辑,支持网络、本地数据库等单一数据源,负责加载指定页码的数据。
- PagingData:分页数据的容器,包裹每一页的数据结果,是数据流的基本单位。
- PagingDataAdapter:RecyclerView 适配器,接收 PagingData,内置 DiffUtil 自动计算差异更新列表,支持加载状态展示。
- RemoteMediator:分层数据源中介,用于「网络 + 本地缓存」的分页场景,负责网络分页数据拉取并写入本地数据库。
- Pager:分页配置入口,配置分页大小、预取距离、PagingSource 工厂,构建 PagingData 数据流。
2. Paging3 相比 Paging2 有哪些核心改进?
答案:
- 响应式流设计:Paging3 基于 Kotlin 协程与 Flow 构建,原生支持 Flow、LiveData、RxJava;Paging2 基于回调,代码冗余且不支持响应式。
- 统一加载状态管理:内置 LoadState 统一处理首次加载、加载更多、加载失败、空状态,无需自行维护状态。
- 解耦设计:PagingSource 与 Adapter 完全解耦,支持任意数据源;Paging2 耦合度高。
- 原生分层缓存支持:RemoteMediator 原生支持网络 + 本地缓存的分页架构,Paging2 需要自行实现缓存逻辑。
- 完善的错误重试:内置 retry 机制,支持全局重试、单页重试;Paging2 重试逻辑需要自行实现。
3. PagingSource 和 RemoteMediator 的区别与适用场景?**
答案:
表格
| 维度 | PagingSource | RemoteMediator |
|---|---|---|
| 角色 | 直接的分页数据源 | 分页中介,协调网络与本地数据源 |
| 数据来源 | 单一数据源(纯网络 / 纯本地) | 双层数据源(网络 + 本地缓存) |
| 触发时机 | 列表滑动到预取阈值时触发加载 | 本地数据加载到底部时触发网络拉取 |
| 结果 | 直接返回 PagingData 给 UI | 将网络数据写入数据库,由本地 PagingSource 驱动 UI 更新 |
适用场景:
- 纯网络分页、无本地缓存:只用 PagingSource。
- 离线优先、网络 + 本地缓存分页:Room 提供 PagingSource + RemoteMediator 处理网络拉取。
4. Paging3 + Room 实现离线分页的完整流程是怎样的? **
答案:
这是 Paging3 最经典的实战场景,遵循单一数据源原则:
- Room DAO 定义返回
PagingSource的查询方法,作为本地分页数据源。 - 实现 RemoteMediator,重写
load()方法:当本地数据加载到底部时,触发网络请求下一页数据。 - 网络请求成功后,将新数据插入 Room 数据库,同时记录分页状态(如 nextKey)。
- Room 数据变更后,自动通过 PagingSource 触发 PagingData 更新,Adapter 自动刷新列表。
- 整个流程 UI 只观察 PagingData 数据流,所有数据变更都来自本地数据库,保证数据一致性。
5. Paging3 的预取(Prefetch)机制是怎样的?如何配置?
答案:
预取是指列表滑动到距离底部还有一定数量的条目时,提前开始加载下一页数据,实现平滑滚动无感知。
- 配置方式 :通过
Pager配置中的prefetchDistance参数设置,默认值等于分页大小pageSize。 - 触发逻辑 :当已加载的最后一个可见条目距离列表末尾的距离小于
prefetchDistance时,触发下一页加载。 - 优化建议:分页大小建议设置为一屏可见数量的 2-3 倍,预取距离设置为 1-2 倍分页大小,平衡加载频率与滑动流畅度。
6. PagingDataAdapter 为什么用 DiffUtil?和普通 Adapter 有什么区别?
答案:
PagingDataAdapter 继承自 RecyclerView.Adapter,内置 DiffUtil 实现增量更新:
- 普通 Adapter 刷新数据通常调用
notifyDataSetChanged(),全量刷新,性能差,无动画。 - PagingDataAdapter 通过 DiffUtil 在后台线程计算新旧数据集的差异,只更新发生变化的 Item,性能更高,且自带 Item 动画。
- PagingDataAdapter 接收 PagingData,自动处理分页加载的增量更新,无需开发者手动处理数据集的合并与差异计算。
7. Paging3 如何处理加载状态与错误重试?
答案:
Paging3 通过 LoadState 统一管理三种加载状态:首次加载(Refresh)、上一页加载(Prepend)、下一页加载(Append),每种状态包含 Loading、Error、NotLoading 三种情况。
- 状态监听 :通过
adapter.loadStateFlow或addLoadStateListener监听加载状态,用于展示加载中、空状态、错误视图。 - 错误重试 :调用
adapter.retry()重试所有失败的加载;也可单独处理首次加载失败的重试。 - 底部加载状态 :可以通过
withLoadStateFooter()给 Adapter 添加底部加载状态的 Footer,自动展示加载中、加载失败、没有更多数据。
8. Paging3 使用中有哪些常见的坑与注意事项?
答案:
- PagingSource 不可复用:PagingSource 是一次性的,数据失效后需要重新创建,因此 Pager 配置传入的是 PagingSource 工厂函数。
- 数据去重:分页加载要注意后端数据重复问题,建议通过唯一 id 去重,避免列表出现重复条目。
- 状态保存:配置变更后 PagingData 会保留,无需重新加载首页,ViewModel 中缓存 Flow 即可。
- 避免内存泄漏:不要在 PagingSource 中持有 Activity/View 上下文,防止内存泄漏。
- RemoteMediator 原子性:写入数据库和更新分页钥匙要放在同一个事务中,保证原子性,避免分页状态错乱。
9. pagingSourceFactory 和 remoteMediator 如何理解这俩参数
pagingSourceFactory 负责「读本地数据,供给 UI 渲染」;remoteMediator 负责「本地不够的时候,去网上拉新数据写入本地」。UI 永远只消费 pagingSource 出来的数据。
pagingSourceFactory
它是PagingSource 的工厂 lambda 。
PagingSource 本身是一次性对象,一旦失效(下拉刷新、数据源变更)就不能复用,所以不能直接 new 一个传进去,要传一个工厂函数,需要的时候 Paging3 自动执行 lambda 生成全新 PagingSource。
在 Room+RemoteMediator 场景,这个 PagingSource 就是 Room DAO 返回的,只做查询本地数据库。列表滑动时 Paging3 从这个 PagingSource 拿数据交给 Adapter 渲染。
职责:读、供给 UI;数据源 = 本地 Room。
remoteMediator
远程中介,是一个附加的后台加载控制器 ,属于 Paging3 的可选参数,纯本地分页不需要它。
触发时机:当pagingSource读取本地数据读到末尾,没有更多本地数据了,Paging3 就会回调 RemoteMediator 的 load 方法,在这里执行网络请求,拿到网络数据写入 Room 数据库 ,同时更新分页 key。
写入成功后,Room 表变化,pagingSource 会感知数据库变更,自动加载新入库的数据,推给 UI。
职责:网络拉取 + 写本地数据库;不直接给 UI 返回数据。
两者协作流程(口述)
- 页面进来,Pager 执行 pagingSourceFactory,创建 Room 的 PagingSource 读取本地缓存数据展示到列表;
- 用户上滑,本地数据读到尽头;
- Paging3 触发 remoteMediator.load (),请求网络下一页;
- RemoteMediator 拿到网络数据,在事务中写入 Room;
- Room 表更新,pagingSource 监听到数据库变化,读取新入库数据,自动交给列表展示。
关键区分点(面试官爱问)
- pagingSource 是数据流主线 ,没有它列表就没有数据渲染;remoteMediator 是后台辅助,只负责填充本地缓存。
- remoteMediator不会直接返回数据给 Adapter,所有 UI 数据都来自 pagingSource(Room)。
- 只有「网络 + 本地缓存、离线优先」架构才需要 remoteMediator;纯网络分页只需要 pagingSourceFactory,不需要 RemoteMediator。
二. 代码demo
1. 数据实体类
kotlin
// 列表条目数据实体,模拟文章数据
data class ArticleBean(
val id: Int, // 唯一主键,用于DiffUtil比对
val title: String, // 标题
val content: String // 内容
)
2. 分页数据源:PagingSource 实现
kotlin
import androidx.paging.PagingSource
import androidx.paging.PagingState
/**
* 分页数据源:负责单页数据的加载逻辑
* 泛型1:页码类型(这里用Int);泛型2:数据条目类型
*/
class ArticlePagingSource(
private val api: ArticleApi // 业务层网络请求接口
) : PagingSource<Int, ArticleBean>() {
/**
* 核心加载方法:每次分页加载都会触发
* params:包含加载页码、每页条数、加载类型等信息
* 返回:成功返回 LoadResult.Page,失败返回 LoadResult.Error
*/
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ArticleBean> {
return try {
// 获取当前页码,首次加载时key为null,使用初始页码1
val currentPage = params.key ?: 1
// 每页加载条目数,由Pager配置的pageSize传入
val pageSize = params.loadSize
// 执行网络请求,获取当前页数据
val response = api.getArticleList(currentPage, pageSize)
val dataList = response.data.list
// 计算上一页页码,第一页没有上一页则为null
val prevKey = if (currentPage > 1) currentPage - 1 else null
// 计算下一页页码,数据为空/条数不足则代表到底,返回null
val nextKey = if (dataList.isNullOrEmpty()) null else currentPage + 1
// 封装分页结果返回给Paging3
LoadResult.Page(
data = dataList,
prevKey = prevKey,
nextKey = nextKey
)
} catch (e: Exception) {
// 加载异常,返回错误结果,Paging3统一管理错误状态
LoadResult.Error(e)
}
}
/**
* 刷新锚点计算:列表刷新后,定位到刷新前的浏览位置
*/
override fun getRefreshKey(state: PagingState<Int, ArticleBean>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
// 模拟的网络请求接口
interface ArticleApi {
suspend fun getArticleList(page: Int, pageSize: Int): ApiResponse<List<ArticleBean>>
}
3. 列表适配器:PagingDataAdapter 实现
kotlin
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
/**
* Paging3专属适配器,内置DiffUtil实现增量更新
* 无需手动管理数据集合并、差异计算
*/
class ArticleAdapter : PagingDataAdapter<ArticleBean, ArticleAdapter.ArticleViewHolder>(DIFF_CALLBACK) {
// DiffUtil比对规则:计算新旧数据差异,只更新变化的条目
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ArticleBean>() {
// 主键比对:判断两个条目是否为同一条数据
override fun areItemsTheSame(oldItem: ArticleBean, newItem: ArticleBean): Boolean {
return oldItem.id == newItem.id
}
// 内容比对:判断同一条数据的内容是否发生变化
override fun areContentsTheSame(oldItem: ArticleBean, newItem: ArticleBean): Boolean {
return oldItem == newItem
}
}
}
class ArticleViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_article, parent, false)
) {
fun bind(bean: ArticleBean?) {
bean?.let {
itemView.tv_title.text = it.title
itemView.tv_content.text = it.content
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
return ArticleViewHolder(parent)
}
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
// getItem()是PagingDataAdapter提供的方法,获取对应位置的数据
holder.bind(getItem(position))
}
}
4. ViewModel 层:Pager 配置与数据流
kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import kotlinx.coroutines.flow.Flow
class ArticleViewModel : ViewModel() {
/**
* 分页数据流:PagingData 的 Flow
* Pager 是Paging3的配置入口,构建分页数据流
*/
val articleFlow: Flow<PagingData<ArticleBean>> = Pager(
config = PagingConfig(
pageSize = 20, // 每页加载的条目数量
prefetchDistance = 40, // 预取距离:距离底部还有多少条时,提前加载下一页
enablePlaceholders = false // 关闭空占位符,列表不显示空白item
),
// PagingSource工厂:每次刷新都会创建新的PagingSource实例
pagingSourceFactory = { ArticlePagingSource(RetrofitClient.articleApi) }
).flow
// 缓存到ViewModel作用域,配置变更(如旋转屏幕)不会重新加载数据
.cachedIn(viewModelScope)
}
5. UI 层:Activity 中使用
kotlin
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class ArticleActivity : AppCompatActivity() {
private val viewModel: ArticleViewModel by viewModels()
private val adapter = ArticleAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_article)
initRecyclerView()
observePagingData()
observeLoadState()
}
private fun initRecyclerView() {
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = adapter
}
/**
* 订阅分页数据流,提交给适配器渲染
*/
private fun observePagingData() {
lifecycleScope.launch {
// collectLatest:只处理最新数据,避免旧数据积压
viewModel.articleFlow.collectLatest { pagingData ->
// 将分页数据提交给适配器,自动增量刷新
adapter.submitData(pagingData)
}
}
}
/**
* 监听加载状态:处理加载中、加载失败、空状态等UI展示
*/
private fun observeLoadState() {
lifecycleScope.launch {
adapter.loadStateFlow.collectLatest { loadStates ->
// loadStates包含三类状态:
// refresh:首次加载/下拉刷新
// append:上滑加载更多
// prepend:加载上一页(极少场景)
val refreshState = loadStates.refresh
// 首次加载中
if (refreshState is androidx.paging.LoadState.Loading) {
showGlobalLoading()
} else {
hideGlobalLoading()
}
// 首次加载失败
if (refreshState is androidx.paging.LoadState.Error) {
showErrorView(refreshState.error.message) {
adapter.retry() // 调用重试
}
}
}
}
}
}
三. Room + RemoteMediator 标准 Demo(离线优先分页架构)
这是 Paging3 最经典的分层缓存架构 ,遵循单一数据源原则:本地 Room 是唯一可信数据源,UI 只观察数据库数据;RemoteMediator 负责在本地数据不足时拉取网络数据并写入数据库,通过 Room 的响应式查询自动刷新列表。
1. 数据库实体类
kotlin
import androidx.room.Entity
import androidx.room.PrimaryKey
/**
* 文章表实体:本地缓存的核心数据
*/
@Entity(tableName = "article_table")
data class ArticleEntity(
@PrimaryKey val id: Int, // 唯一主键,用于DiffUtil和分页关联
val title: String,
val content: String,
val publishTime: Long
)
/**
* 分页钥匙表:记录每条数据对应的分页页码
* 用于精准定位上一页/下一页,比全局存页码更可靠,支持刷新后位置恢复
*/
@Entity(tableName = "remote_keys_table")
data class RemoteKeysEntity(
@PrimaryKey val articleId: Int, // 与文章表主键一一对应
val prevKey: Int?, // 上一页页码
val nextKey: Int? // 下一页页码
)
2. DAO 数据访问接口
kotlin
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
@Dao
interface ArticleDao {
/**
* 分页查询:返回PagingSource,Room原生支持
* 表数据变更时自动触发PagingSource更新,UI自动刷新
*/
@Query("SELECT * FROM article_table ORDER BY publishTime DESC")
fun getArticlePagingSource(): PagingSource<Int, ArticleEntity>
/**
* 批量插入文章数据
*/
@Insert
suspend fun insertArticles(articles: List<ArticleEntity>)
/**
* 清空文章表(刷新时用)
*/
@Query("DELETE FROM article_table")
suspend fun clearArticles()
}
@Dao
interface RemoteKeysDao {
/**
* 根据文章ID查询对应的分页钥匙
*/
@Query("SELECT * FROM remote_keys_table WHERE articleId = :articleId")
suspend fun getRemoteKeyByArticleId(articleId: Int): RemoteKeysEntity?
/**
* 批量插入分页钥匙
*/
@Insert
suspend fun insertRemoteKeys(keys: List<RemoteKeysEntity>)
/**
* 清空分页钥匙表
*/
@Query("DELETE FROM remote_keys_table")
suspend fun clearRemoteKeys()
}
3. Room 数据库类
kotlin
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(
entities = [ArticleEntity::class, RemoteKeysEntity::class],
version = 1,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun articleDao(): ArticleDao
abstract fun remoteKeysDao(): RemoteKeysDao
}
4. 模拟网络请求接口
kotlin
data class ApiResponse<T>(
val data: List<T>,
val page: Int,
val total: Int
)
interface ArticleApi {
/**
* 网络分页请求
* @param page 页码
* @param pageSize 每页条数
*/
suspend fun getArticleList(page: Int, pageSize: Int): ApiResponse<ArticleEntity>
}
5. 核心:RemoteMediator 实现
kotlin
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
@OptIn(ExperimentalPagingApi::class)
class ArticleRemoteMediator(
private val database: AppDatabase,
private val api: ArticleApi
) : RemoteMediator<Int, ArticleEntity>() {
private val articleDao = database.articleDao()
private val keysDao = database.remoteKeysDao()
/**
* 核心加载方法:分页加载触发时回调
* @param loadType 加载类型:REFRESH刷新、APPEND加载更多、PREPEND加载上一页
* @param state 当前分页状态,包含已加载数据、分页配置等
* @return MediatorResult:成功/失败,成功时标记是否已到最后一页
*/
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, ArticleEntity>
): MediatorResult {
return try {
// 1. 根据加载类型,计算当前请求的页码
val currentPage = when (loadType) {
// 首次加载/下拉刷新:从第1页开始
LoadType.REFRESH -> 1
// 上滑加载更多:取最后一条数据对应的下一页页码
LoadType.APPEND -> {
// 获取已加载的最后一条数据
val lastItem = state.lastItemOrNull()
?: return MediatorResult.Success(endOfPagination = true)
// 查询该数据对应的下一页页码
val nextKey = keysDao.getRemoteKeyByArticleId(lastItem.id)?.nextKey
?: return MediatorResult.Success(endOfPagination = true)
nextKey
}
// 加载上一页:普通列表场景极少用到,直接返回无更多数据
LoadType.PREPEND -> return MediatorResult.Success(endOfPagination = true)
}
// 2. 执行网络请求,获取分页数据
val pageSize = state.config.pageSize
val response = api.getArticleList(currentPage, pageSize)
val dataList = response.data
val isEndOfList = dataList.isEmpty()
// 3. 数据库事务:写入数据 + 更新分页钥匙,必须原子操作
database.withTransaction {
// 刷新场景:先清空旧数据和旧钥匙
if (loadType == LoadType.REFRESH) {
articleDao.clearArticles()
keysDao.clearRemoteKeys()
}
// 计算每条数据对应的上一页/下一页页码
val prevKey = if (currentPage > 1) currentPage - 1 else null
val nextKey = if (isEndOfList) null else currentPage + 1
val remoteKeys = dataList.map { article ->
RemoteKeysEntity(
articleId = article.id,
prevKey = prevKey,
nextKey = nextKey
)
}
// 批量插入数据和分页钥匙
keysDao.insertRemoteKeys(remoteKeys)
articleDao.insertArticles(dataList)
}
// 4. 返回加载结果,标记是否已到最后一页
MediatorResult.Success(endOfPagination = isEndOfList)
} catch (e: Exception) {
// 加载异常,返回错误结果,Paging3统一管理重试
MediatorResult.Error(e)
}
}
/**
* 初始化加载策略:可选配置
* 默认返回LAUNCH_INITIAL_REFRESH,进入页面自动触发首次刷新
*/
override suspend fun initialize(): InitializeAction {
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
}
6. ViewModel 层
kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import kotlinx.coroutines.flow.Flow
class ArticleViewModel : ViewModel() {
// 注入数据库和API实例(实际项目用Hilt注入)
private val database = AppDatabase.getInstance()
private val api = RetrofitClient.articleApi
/**
* 分页数据流
* 配置remoteMediator后,分页逻辑由「本地PagingSource + RemoteMediator」共同驱动
*/
val articleFlow: Flow<PagingData<ArticleEntity>> = Pager(
// 分页核心配置
config = PagingConfig(
pageSize = 20, // 每页加载条目数量,同时作为预取计算基准
prefetchDistance = 40, // 预取距离:距离列表底部还剩40条时,提前触发下一页加载,做到滑动无感
enablePlaceholders = false // 关闭占位符,不渲染空白item;开启时会预先绘制空占位,适合已知总条数场景
),
// 远程中介:处理网络分页逻辑。本地数据加载完毕后,RemoteMediator自动触发网络请求拉取下一页,写入Room
remoteMediator = ArticleRemoteMediator(database, api),
// PagingSource工厂函数:每次刷新/重建分页时,生成新的本地PagingSource,从Room查询数据
// 注意:必须传【工厂lambda】,不能直接传实例,因为PagingSource是一次性对象,失效后需要新建
pagingSourceFactory = { database.articleDao().getArticlePagingSource() }
)
.flow // Pager暴露flow,发射PagingData分页数据包
.cachedIn(viewModelScope) // 将Flow缓存到ViewModel协程作用域。屏幕旋转等配置变更时,复用已有分页数据,不重新请求网络/数据库
}
7. UI 层(与纯网络版完全一致)
Adapter 直接复用纯网络分页的 PagingDataAdapter,Activity 订阅数据流的逻辑也完全相同,UI 层完全感知不到网络层的存在,只与本地数据库交互。
kotlin
// 核心订阅逻辑,其余代码与纯网络版一致
private fun observePagingData() {
lifecycleScope.launch {
viewModel.articleFlow.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
}
架构核心总结(面试可直接说)
- 单一数据源:UI 只从 Room 取数据,网络数据仅写入数据库,保证在线 / 离线数据一致性。
- 职责分离:PagingSource 负责本地分页,RemoteMediator 负责网络拉取与缓存写入,互不耦合。
- 原子性保证:数据写入与分页钥匙更新放在同一事务,避免分页状态错乱。
- 响应式驱动:Room 数据变更自动触发列表刷新,无需手动同步网络数据与 UI 数据。