96SEO 2026-08-09 23:47 1
痛点:手动实现分页时你需要自行维护页码、加载状态、错误处理、重复请求防护等,一不小心就会出现“分页祖传代码”。每个列表都重复这些逻辑,维护成本极高。

前面我们写过课程列表,大多是这样:
val courses = listOf(
Course,Course,Course
)
这种固定列表适合 Demo,但真实业务里列表通常是从接口分页加载的,例如:
如果手动做分页。你需要自己处理:
Paging 是 Android 官方提供的分页库,专门处理这些问题。
| 手动分页 | Paging |
|---|---|
自己维护 page
PagingSource | LazyPagingItems |
自己判断加载更多
PagingSource.key | LoadState |
自己写重试逻辑
.retry | .retry |
自己写刷新逻辑
.refresh | .refresh |
| 自己防重复请求 | Paging 内部处理大部分场景 |
Paging 在 Compose 中的数据流大概是这样:
flowchart LR
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
PagingSource 负责怎么加载一页数据。Pager 把分页数据变成 Flow,Compose 把 Flow 显示成列表。
libs.versions.toml
agp = "8.5.0"
kotlin = "1.9.24"
composeBom = "2024.08.00"
activity = "1.9.0"
lifecycle = "2.8.0"
paging = "3.3.0"
coroutines = "1.8.0"
androidx-compose-bom = { module = "androidx.compose:compose-bom",version.ref = "composeBom" }
androidx-activity-compose = { module = "androidx.activity:activity-compose",version.ref = "activity" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose",version.ref = "lifecycle" }
androidx-paging-runtime = { module = "androidx.paging:paging-runtime",version.ref = "paging" }
androidx-paging-compose = { module = "androidx.paging:paging-compose",version.ref ="paging"}
kotlinx-coroutines-android= { module ="org.jetbrains.kotlinx:kotlinx-coroutines-android",version.ref ="coroutines"}
android-application ={ id ="com.android.application",version.ref ="agp"}
kotlin-android ={ id ="org.jetbrains.kotlin.android",version.ref ="kotlin"}
compose-compiler ={ id ="org.jetbrains.kotlin.plugin.compose"。version.ref ="kotlin"}
app/build.gradle.kts
plugins {
alias
alias
alias
}
android {
namespace = "com.example.pagingdemo"
compileSdk = 35
defaultConfig {
applicationId = "com.example.pagingdemo"
minSdk = 21
targetSdk = 35
versionCode = 1
versionName ="1.0"
}
buildFeatures { compose=true }
}
dependencies {
implementation)
implementation
implementation
implementation
implementation
implementation
implementation
implementation
}
Pain Point:If you forget to add alert,compiler will complain about missing PagedListAdapter ,leading to hours of debugging.
The first step is to implement a PAGINGSource .
data class Course(
val id: Int,val title: String,val description: String
)
import androidx.paging.PagingSource import androidx.paging.PagingState import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay class CoursePagingSource : PagingSource { override suspend fun load: LoadResult{ return try { // 没有 key 时默认从第 1 页开始 val page : Int = params.key?: 1 val loadSize : Int = params.loadSize // 模拟网络延迟 delay // 第三页演示一次故意失败,用于展示错误 UI if throw IllegalStateException val courses : List = createCourses LoadResult.Page( data : courses。prevKey : if null else page - 1,nextKey : if null else page + 1 ) } catch { // 协程被取消不算错误,需要重新抛出,让 Paging 正确处理取消逻辑 throw cancel } catch { LoadResult.Error } } override fun getRefreshKey: Int?{ // 当使用者下拉刷新或调用 refresh 时依据当前可见位置决定从哪一页重新请求 val anchorPos : Int?= state.anchorPosition?说起来,: return null val anchorPage : PagingState.Page ?= state.closestPageToPosition?: return null return anchorPage.prevKey?.plus,: anchorPage.nextKey?.minus } }
/** 简单的假数据生成器。仅用于 Demo */ private const val TOTAL_PAGE = 5
private fun createCourses: List
{ val startId= *pageSize +1 return List{ index -> val id= startId+index Course( id=id,title= "Compose课程 $id",description= "第 $page 页的第 ${index+1} 条课程内容。" ) } } The key take‑aways:
- The generic type `
` means “key is an `Int` page number”. - `params.key?说起来,: 1` handles initial load when no key exists.
- `params.loadSize` gives exact number of items requested for this call – it may differ from `pageSize` on very first request.
- `LoadResult.Page` carries three essential pieces of information:
- `data` – current page items.
- `prevKey` – key for previous page .
` nextKey` – key for next page .`CancellationException` 必须原样抛出。否则 UI 会误报“append 错误”。 `getRefreshKey` 决定 `refresh` 时从哪个页面重新请求,保证使用者下拉刷新后仍停留在可见位置附近。 四、Pager:把 PagingSource 包装成 Flow
import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.Pager import androidx.pagination.PaginationConfig?,?// actually it's androidx.pagin... import androidx.p paging.PaginationData?,// correct import import kotlinx.coroutines.flow.Flow class CourseListViewModel : ViewModel { // 👉️关键:这里使用 cachedIn。让同一个 ViewModel 多次收集时共享同一条 Flow,// 否则旋转屏幕会重新发起一次全新的分页请求。val courses: Flow= Pager( config= PagingConfig( pageSize =,initialLoadSize =,prefetchDistance =,enablePlaceholders=false )。pagingSourceFactory={ CoursePagingSource } ).flow.cachedIn }
The configuration parameters you most often tweak:
参数 作用 pageSize 每次向后请求多少条 initialLoadSize 首次渲染时想一次性拿多少条 prefetchDistance 滚动到距离底部还有多少条时提前预取下一页 enablePlaceholders 是否显示占位 item;大多数网络接口不返回总数,建议关掉。说起来, "Why cachedIn?" Because `` Each collector would launch a brand‑new pagination pipeline orwise – causing duplicated network calls and losing scroll position after configuration changes.
五、Compose 中显示 PagingData
@Composable fun CourseListRoute){ // 将 Flow 转换为 LazyPagingItems,这一步必须在 Composable 中完成 val courses= viewModel.courses.collectAsLazyPagingItems CourseListScreen } kotlin @Composable fun CourseListScreen{ when{ is LoadState.Loading -> LoadingContent is LoadState.Error -> ErrorContent( message=refreshState.error.message? :"课程加载失败",onRetryClick={courses.retry} ) is LoadState.NotLoading ->{ if{ EmptyContent}) }else{ CoursePagingList } } }The list itself:
kotlin @Composable fun CoursePagingList{ LazyColumn{ items( count=courses.itemCount。key=courses.itemKey{ it.id } // 稳定的唯一 key 防止 recomposition 错位 ){ index -> courses?.let{ course->CourseItem } }
// Footer 用来展示 append 状态 item{ AppendLoadStateFooter( appendState=courses.loadState.append,onRetryClick={courses.retry} ) } }LoadState:统一处理 “首屏/底部” 各种 UI 状态
kotlin @Composable fun AppendLoadStateFooter->Unit){ Column( modifier=Modifier.fillMaxWidth.padding。horizontalAlignment=Alignment.CenterHorizontally,verticalArrangement=Arrangement.spacedBy){
when{ is LoadState.Loading -> Text is LoadState.Error ->{ Text OutlinedButton{ Text } } is LoadState.NotLoading ->{ if{ Text }else{ Text } } } }
loadState.refresh→ 首屏或下拉刷新的整体状态。loadState.append→ 底部“向下”继续分页的状态。其实,- 区分
Loading / NotLoading / Error并对应 UI。- 调用
lazyPagingItems.retry重试最近一次失败;调用lazyPagi ngItems.refresh完全重新拉取,从第一页开始。
常见错误速查表
错误表现 根本原因 每次页面重组都会重新发起网络请求,滚动位置被强制回到顶部。 Pager 写在 @Composable 中,没有使用 cachedIn;老实说,每次 collect 都创建新 flow。 底部一直提示 “继续向下滑动”,但已经没有更多数据。 未检查 append.endOfPaginationReached;nextKey 被错误设置为非 null。说起来, 点击“重试”后仍然卡在 Loading 状态。 把 CancellationException 当作普通异常返回了。正确做法是直接 throw cancel。 六、综合实战:完整可运行的课程分页 Demo
package com.example.pagingdemo import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel //--- Data Model ------------------------------------------------------------- data class Course //--- Paging Source ----------------------------------------------------------- class CoursePagingSource : PagingSource { override suspend fun load:LoadResult{ return try{ val page = val size = delay // 模拟网络延迟 if{ // 第三页演示故障场景。仅用于 demo throw IllegalStateException } val list=createCourses LoadResult.Page( data=list,prevKey=if null else page-1,nextKey=if null else page+1 ) }catch{ throw e // 必须原样抛出,不然会被当成普通错误显示 }catch{ LoadResult.Error } override fun getRefreshKey:Int?{ val pos = val pageInfo=?:return null)
return pageInfo.prevKey?.plus,: pageInfo.nextKey?.minus//--- 假数据生成 ------------------------------------------------------------- private const val TOTAL_PAGE=5
private fun createCourses:List
{ val start=*size+1 return List{ idx -> val id=start+idx Course } } //--- ViewModel --------------------------------------------------------------- class CourseListViewModel : ViewModel{ val courses= Pager(PagingConfig(pageSize=10,initialLoadSize=10,prefetchDistance=2。enablePlaceholders=false)){ CoursePagingSource }.flow.cachedIn }
//--- UI ---------------------------------------------------------------------- @Composable fun CourseListRoute){ val pagingItems=vm.courses.collectAsLazyPagingItems CourseListScreen }
@Composable fun CourseListScreen{
Scaffold{ innerPadding -> Box.padding){ when{ is LoadS tate.Loading -> LoadingContent is LoadS tate.Error -> ErrorContent(message= rs.error.message?:\"课程读取失败\",onRetryClick={pages.retry}) is LoadS tate.NotLoading ->{ if{ EmptyContent}) }else{ CoursesList } } } } }@Composable fun CoursesList{ LazyColumn。verticalArrangement= Arrangement.spacedBy){
item{ Text(\"课程列表\",style= MaterialTheme.typography.headlineMedium)} items(count=pages.itemCount,key= pages.itemKe y{it.id}){ idx -> pages?.let{ course->CourseItem} } item{ AppendLoadStat eFooter(appendSt ate= pages.loadSt ate.append,onRetryClick={pages.retry}) } }@Composable fun CourseItem{ Card(modifier= Modifier.fillMaxWidth,elevation= CardDefaults.cardElevation){ Column(modifier= Modifier.padding。verticalArrangement= Arrangement.spacedBy){ Text(course.title,style= MaterialTheme.typography.titleMedium ) Text(course.description,style= MaterialTheme.typography.bodyMedium ) } } }
when{ is L oadSta te.Loading->Text is L oadSta te.Error->{ Text(text= appendStat.error.message?:\"加 載失敗\") OutlinedButton{ Text }} is L oadSta te.NotLoading->{ if{ Text(\"— 已经到底了 —\",style= MaterialTheme.typography.bodySmall ) }else{ Text }} }//when@Composable fun LoadingConte nt(text:String,modifier: Modi fier=M odifier.padding){ Column,horizontalAlignment = Alignment.CenterHorizontally ){ CircularProgressIndicator Spacer(modifier. Modifier.height) Text } }
@Composable fun ErrorConte nt(message:String,onRetryC lick: ->U nit。modifier: Modi fier=M odifier.padding){ Column(horizontalAlignment = Alignment.CenterHorizontally ){ Text Button} ){ Tex t } } }
@Composable fun EmptyConte nt(onRefreshC lick: ->U nit,modifier: Modi fier=M odifier.padding){
Column(horizontalAlignment = Alignment.CenterHorizontally ){ Tex t Button} ){ Tex t } }//colThis demo demonstrates:
- 首屏自动触发 Refresh 并显示 Loading/Error/Empty 状态。
- 滚动到底部自动触发 Append,实现无限滚动。
- 模拟第 三页故障,用于展示底部错误并支持 Retry。
- 使用
cachedIn保证旋转屏幕后不会重新请求已缓存的数据。
定义分页源 class XxxPagi ngSou rce:</t d td class="">① ③ ④ ⑤ ⑥ 三 ...
七、Paging 快速查表
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback