谷歌SEO

谷歌SEO

Products

当前位置:首页 > 谷歌SEO >

Jetpack Compose 的 Paging 3 如何实现分页加载?

96SEO 2026-08-09 23:47 1


Jetpack Compose 入门系列:Paging 分页加载

一、为什么需要 Paging

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

Jetpack Compose 的 Paging 3 如何实现分页加载?

前面我们写过课程列表,大多是这样:

val courses = listOf(
Course,Course,Course
)

这种固定列表适合 Demo,但真实业务里列表通常是从接口分页加载的,例如:

  • 商品列表
  • 课程列表
  • 评论列表
  • 消息列表
  • 搜索结果

如果手动做分页。你需要自己处理:

  • 当前第几页
  • 是否正在加载
  • 是否还有下一页
  • 首屏加载错误
  • 底部加载错误
  • 刷新与重试
  • 防止重复请求
  • 滑动到底部自动加载下一页

Paging 是 Android 官方提供的分页库,专门处理这些问题。

手动分页Paging
自己维护 page PagingSourceLaz​yPagingItems
自己判断加载更多 PagingSource.keyL​oadState
自己写重试逻辑 .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 显示成列表。

二、添加 Paging 依赖

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.

三、PagingSource:告诉 Paging 怎么加载一页数据 

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 完全重新拉取,从第一页开始。

    常见错误速查表 

    未检查 append.endOfPaginationReached;nextKey 被错误设置为非 null。说起来,点击“重试”后仍然卡在 Loading 状态。把 CancellationException 当作普通异常返回了。正确做法是直接 throw cancel。
    错误表现 根本原因
    每次页面重组都会重新发起网络请求,滚动位置被强制回到顶部。Pager 写在 @Composable 中,没有使用 cachedIn;老实说,每次 collect 都创建新 flow。
    底部一直提示 “继续向下滑动”,但已经没有更多数据。

    六、综合实战:完整可运行的课程分页 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
    }
    }//col
    

    This demo demonstrates:

    • 首屏自动触发 Refresh 并显示 Loading/Error/Empty 状态。
    • 滚动到底部自动触发 Append,实现无限滚动。
    • 模拟第 三页故障,用于展示底部错误并支持 Retry。
    • 使用 cachedIn 保证旋转屏幕后不会重新请求已缓存的数据。

    定义分页源
    class Xxx​Pagi ngSou rce:<​/t d td class="">① ③ ④ ⑤ ⑥ 三 ...

    七、Paging 快速查表 


    标签: 分页

    SEO优化服务概述

    作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。

    百度官方合作伙伴 白帽SEO技术 数据驱动优化 效果长期稳定

    SEO优化核心服务

    网站技术SEO

    • 网站结构优化 - 提升网站爬虫可访问性
    • 页面速度优化 - 缩短加载时间,提高用户体验
    • 移动端适配 - 确保移动设备友好性
    • HTTPS安全协议 - 提升网站安全性与信任度
    • 结构化数据标记 - 增强搜索结果显示效果

    内容优化服务

    • 关键词研究与布局 - 精准定位目标关键词
    • 高质量内容创作 - 原创、专业、有价值的内容
    • Meta标签优化 - 提升点击率和相关性
    • 内容更新策略 - 保持网站内容新鲜度
    • 多媒体内容优化 - 图片、视频SEO优化

    外链建设策略

    • 高质量外链获取 - 权威网站链接建设
    • 品牌提及监控 - 追踪品牌在线曝光
    • 行业目录提交 - 提升网站基础权威
    • 社交媒体整合 - 增强内容传播力
    • 链接质量分析 - 避免低质量链接风险

    SEO服务方案对比

    服务项目 基础套餐 标准套餐 高级定制
    关键词优化数量 10-20个核心词 30-50个核心词+长尾词 80-150个全方位覆盖
    内容优化 基础页面优化 全站内容优化+每月5篇原创 个性化内容策略+每月15篇原创
    技术SEO 基本技术检查 全面技术优化+移动适配 深度技术重构+性能优化
    外链建设 每月5-10条 每月20-30条高质量外链 每月50+条多渠道外链
    数据报告 月度基础报告 双周详细报告+分析 每周深度报告+策略调整
    效果保障 3-6个月见效 2-4个月见效 1-3个月快速见效

    SEO优化实施流程

    我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:

    1

    网站诊断分析

    全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。

    2

    关键词策略制定

    基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。

    3

    技术优化实施

    解决网站技术问题,优化网站结构,提升页面速度和移动端体验。

    4

    内容优化建设

    创作高质量原创内容,优化现有页面,建立内容更新机制。

    5

    外链建设推广

    获取高质量外部链接,建立品牌在线影响力,提升网站权威度。

    6

    数据监控调整

    持续监控排名、流量和转化数据,根据效果调整优化策略。

    SEO优化常见问题

    SEO优化一般需要多长时间才能看到效果?
    SEO是一个渐进的过程,通常需要3-6个月才能看到明显效果。具体时间取决于网站现状、竞争程度和优化强度。我们的标准套餐一般在2-4个月内开始显现效果,高级定制方案可能在1-3个月内就能看到初步成果。
    你们使用白帽SEO技术还是黑帽技术?
    我们始终坚持使用白帽SEO技术,遵循搜索引擎的官方指南。我们的优化策略注重长期效果和可持续性,绝不使用任何可能导致网站被惩罚的违规手段。作为百度官方合作伙伴,我们承诺提供安全、合规的SEO服务。
    SEO优化后效果能持续多久?
    通过我们的白帽SEO策略获得的排名和流量具有长期稳定性。一旦网站达到理想排名,只需适当的维护和更新,效果可以持续数年。我们提供优化后维护服务,确保您的网站长期保持竞争优势。
    你们提供SEO优化效果保障吗?
    我们提供基于数据的SEO效果承诺。根据服务套餐不同,我们承诺在约定时间内将核心关键词优化到指定排名位置,或实现约定的自然流量增长目标。所有承诺都会在服务合同中明确约定,并提供详细的KPI衡量标准。

    SEO优化效果数据

    基于我们服务的客户数据统计,平均优化效果如下:

    +85%
    自然搜索流量提升
    +120%
    关键词排名数量
    +60%
    网站转化率提升
    3-6月
    平均见效周期

    行业案例 - 制造业

    • 优化前:日均自然流量120,核心词无排名
    • 优化6个月后:日均自然流量950,15个核心词首页排名
    • 效果提升:流量增长692%,询盘量增加320%

    行业案例 - 电商

    • 优化前:月均自然订单50单,转化率1.2%
    • 优化4个月后:月均自然订单210单,转化率2.8%
    • 效果提升:订单增长320%,转化率提升133%

    行业案例 - 教育

    • 优化前:月均咨询量35个,主要依赖付费广告
    • 优化5个月后:月均咨询量180个,自然流量占比65%
    • 效果提升:咨询量增长414%,营销成本降低57%

    为什么选择我们的SEO服务

    专业团队

    • 10年以上SEO经验专家带队
    • 百度、Google认证工程师
    • 内容创作、技术开发、数据分析多领域团队
    • 持续培训保持技术领先

    数据驱动

    • 自主研发SEO分析工具
    • 实时排名监控系统
    • 竞争对手深度分析
    • 效果可视化报告

    透明合作

    • 清晰的服务内容和价格
    • 定期进展汇报和沟通
    • 效果数据实时可查
    • 灵活的合同条款

    我们的SEO服务理念

    我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。

    提交需求或反馈

    Demand feedback