PagingSource 与 Pager
定义页面的加载方式
PagingSource 与 Pager 是 CoddyKit 上的免费 Android Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Android Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Android Academy 课程共包含 4 节课。
定义页面的加载方式
要从网络 API 分页获取数据,需要编写一个 PagingSource。它需要回答 Paging 3 的两个问题:
- 如何加载指定键对应的页面?
- 用户刷新时,应从哪里继续加载?
然后,将它包装在一个 Pager 中,将其转换为 Flow<PagingData>。
PagingSource 类型参数
PagingSource<Key, Value> 接受两个类型参数:
- Key - 用于标识页面。对于页码 API,它是
Int;对于游标 API,它是一个String令牌。 - Value - 项目类型,例如
Article。
import androidx.paging.PagingSource
import androidx.paging.PagingState
class ArticlePagingSource(
private val api: ArticleApi
) : PagingSource<Int, Article>() {
// implement load() and getRefreshKey()
}实现 load()
load() 是一个 suspend 函数。它接收 params.key(要获取的页面),并返回一个 LoadResult。
成功时,返回包含项目以及上一页和下一页键的 LoadResult.Page。某个键为 null,表示该方向没有页面。
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, Article> {
val page = params.key ?: 1 // first load has a null key
return try {
val response = api.getArticles(page = page, size = params.loadSize)
LoadResult.Page(
data = response.items,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.items.isEmpty()) null else page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}为什么 prevKey 和 nextKey 很重要
用户向下滚动时,Paging 使用 nextKey 向前加载;使用 prevKey 向后加载(从列表中间开始时很有用)。
将 nextKey 返回为 null 会告诉 Paging 没有更多页面,因此它会停止请求。忘记这一点可能导致无限的空请求。
// Stop forward paging when the server returns an empty page
nextKey = if (response.items.isEmpty()) null else page + 1
// Stop backward paging at the first page
prevKey = if (page == 1) null else page - 1实现 getRefreshKey()
列表刷新时(下拉刷新或失效),Paging 需要知道应重新加载哪个页面,以便用户大致停留在原来的位置。
getRefreshKey() 使用当前的 anchorPosition(距离视口最近的项目)来选择合理的键。
override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
return state.anchorPosition?.let { anchor ->
val closestPage = state.closestPageToPosition(anchor)
closestPage?.prevKey?.plus(1)
?: closestPage?.nextKey?.minus(1)
}
}妥善处理错误
请将网络调用放在 try/catch 中,并在失败时返回 LoadResult.Error(e)。Paging 会在界面中将其显示为 LoadState.Error,这样您就可以显示重试按钮。
不要让异常从 load() 中逸出 - 请捕获异常,并将其转换为 LoadResult.Error。
return try {
val response = api.getArticles(page = page, size = params.loadSize)
LoadResult.Page(
data = response.items,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.items.isEmpty()) null else page + 1
)
} catch (e: IOException) { // no network
LoadResult.Error(e)
} catch (e: HttpException) { // non-2xx response
LoadResult.Error(e)
}创建 Pager
Pager 将您的 PagingConfig 与一个用于创建全新 PagingSource 的工厂连接起来。它的 .flow 属性是一个由界面收集的 Flow<PagingData>。
工厂 lambda 每次都必须创建一个新的数据源,因为 Paging 会在刷新时使当前数据源失效并重新创建它。
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow
class ArticleRepository(private val api: ArticleApi) {
fun articleStream(): Flow<PagingData<Article>> = Pager(
config = PagingConfig(pageSize = 20, prefetchDistance = 5),
pagingSourceFactory = { ArticlePagingSource(api) }
).flow
}用于 ViewModel 的 cachedIn
收集 PagingData 是一次性操作;再次收集会重新开始加载。为了在配置更改后继续工作,并让多个收集者共享数据,请通过 cachedIn 将数据流缓存到 viewModelScope 中。
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
class ArticleViewModel(
repo: ArticleRepository
) : ViewModel() {
val articles = repo.articleStream()
.cachedIn(viewModelScope)
}loadSize 与 pageSize
请注意,load() 读取的是 params.loadSize,而不是您配置的 pageSize。
第一次加载时,Paging 可能会请求更大的初始数据块(由 PagingConfig 中的 initialLoadSize 控制,默认是页面大小的 3 倍)。请始终将 params.loadSize 传给您的 API,以便请求大小与 Paging 期望返回的数据量一致。
// Correct: respect the size Paging asks for
val response = api.getArticles(page = page, size = params.loadSize)
// PagingConfig can tune the first load:
PagingConfig(pageSize = 20, initialLoadSize = 40)基于游标的 API
并非所有 API 都使用页码。有些 API 会返回指向下一页的游标或令牌。模式完全相同 - 只需将 Key 类型改为 String,并使用响应中的令牌。
class CursorArticleSource(
private val api: ArticleApi
) : PagingSource<String, Article>() {
override suspend fun load(
params: LoadParams<String>
): LoadResult<String, Article> {
val cursor = params.key // null on first load
return try {
val res = api.getArticles(cursor = cursor, size = params.loadSize)
LoadResult.Page(
data = res.items,
prevKey = null, // forward-only cursor
nextKey = res.nextCursor // null when exhausted
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<String, Article>) = null
}整合起来
现在您已经拥有完整的数据层:一个加载单个页面的 PagingSource、一个流式传送页面的 Pager,以及一个缓存数据流的 ViewModel。
界面层只需收集 viewModel.articles - 下一课将使用 Compose LazyColumn 呈现这些内容。
// Data layer summary
// 1. ArticlePagingSource : PagingSource<Int, Article>
// 2. Pager(config, factory).flow -> Flow<PagingData<Article>>
// 3. ViewModel: repo.articleStream().cachedIn(viewModelScope)
// UI just collects viewModel.articles快速检查
在基于页码的 PagingSource 中,从 load() 返回 nextKey = null 表示什么?
回顾:PagingSource 与 Pager
您已经构建了分页所需的数据层:
PagingSource<Key, Value>实现load()和getRefreshKey()load()返回带有prevKey/nextKey的LoadResult.Page,或返回LoadResult.Error- 将键设为
null会停止该方向上的分页 Pager(config, factory).flow生成一个Flow<PagingData>cachedIn(viewModelScope)会在配置更改后继续保留数据
下一步:在 Compose 列表中呈现这个数据流。
用 AI 导师学习 Kotlin — 免费
在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。
- 课程
- 36
- 课程
- 152
常见问题解答
「PagingSource 与 Pager」课时是免费的吗?
是的 — 「PagingSource 与 Pager」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Android Academy 课程的其余内容,请升级到 CoddyKit PRO。 Android Academy 课程共包含 4 节课。
「PagingSource 与 Pager」这节课中我会学到什么?
定义页面的加载方式 你通过在浏览器中直接运行的动手代码来练习 Android Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Android Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Android Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「PagingSource 与 Pager」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Android Academy 课中编写并运行代码吗?
能。每节 Android Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 为什么需要分页
- PagingSource 与 Pager
- 在 Compose 列表中实现分页
- RemoteMediator 与缓存