一步到位学 Compose + Paging3:从 0 到 1 实现分页加载(超详细新手教程)

Jetpack Compose 是 Android 现代 UI 开发的首选,Paging3 是官方专门做分页加载的库,两者结合能轻松实现列表分页、下拉刷新、加载状态监听等刚需功能。

一、先搞懂:Paging3 是什么?核心角色有哪些?

1. Paging3 是干嘛的?

专门帮你处理列表分页加载

  • 自动加载下一页数据
  • 处理下拉刷新
  • 监听加载状态(加载中、加载失败、无更多数据)
  • 内存优化,避免列表卡顿
  • 完全适配 Kotlin 协程 + Flow,和 Compose 天生一对

2. Paging3 核心 4 大组件(必须记住!)

表格

组件 作用
PagingSource 核心!负责从网络 / 数据库加载单页数据,定义分页逻辑
PagingConfig 配置分页参数(每页加载多少、预加载距离等)
Pager 把 PagingSource 和 PagingConfig 组合,输出 Flow
PagingDataAdapter/LazyPagingItems Compose 中用它展示列表,接收分页数据

一句话流程:数据来源PagingSource(加载单页)→ Pager(生成分页流)→ ViewModel(管理数据)→ Compose(展示列表)

二、环境准备:添加依赖

先在 build.gradle(Module) 中添加 Paging3 + Compose 依赖,用最新版本即可:

gradle

arduino 复制代码
// paging3 核心依赖
implementation "androidx.paging:paging-runtime-ktx:3.2.1"
// compose 扩展适配
implementation "androidx.paging:paging-compose:3.2.1"

// 可选:协程
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
// 可选:网络请求(示例用)
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"

三、实战步骤:5 步完成 Compose + Paging3

我们用公开的接口做示例(无需自己搭服务),实现一个文章列表的分页加载。

步骤 1:定义数据实体类

先创建一个数据类,对应接口返回的数据结构:

kotlin

kotlin 复制代码
// 文章实体类
data class Article(
    val id: Int,
    val title: String,
    val shareUser: String
)

// 接口返回的外层结构(根据实际接口调整)
data class BaseResponse<T>(
    val data: DataBean<T>,
    val errorCode: Int
)

data class DataBean<T>(
    val curPage: Int, // 当前页
    val datas: List<T>, // 数据列表
    val pageCount: Int, // 总页数
    val size: Int, // 每页数量
)

步骤 2:创建 Api 接口(网络请求)

用 Retrofit 定义分页请求接口,我们用 WanAndroid 公开接口:

kotlin

kotlin 复制代码
interface ApiService {
    // 分页获取文章列表,page 从 0 开始
    @GET("article/list/{page}/json")
    suspend fun getArticles(@Path("page") page: Int): BaseResponse<List<Article>>
}

// Retrofit 实例
object RetrofitClient {
    private const val BASE_URL = "https://www.wanandroid.com/"

    val api: ApiService by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }
}

步骤 3:核心!编写 PagingSource

这是 Paging3 最重要的部分,负责加载每一页数据 。我们需要继承 PagingSource<Key, Value>

  • Key:分页页码(一般用 Int)
  • Value:列表数据实体(这里是 Article)

kotlin

kotlin 复制代码
class ArticlePagingSource(
    private val api: ApiService
) : PagingSource<Int, Article>() {

    // 1. 刷新时重置页码
    override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
        // 刷新时从第一页重新加载
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey ?: state.closestPageToPosition(anchorPosition)?.nextKey
        }
    }

    // 2. 核心:加载单页数据
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        return try {
            // params.key:当前页码,首次加载为 null
            val currentPage = params.key ?: 0
            // 发起网络请求
            val response = api.getArticles(currentPage)
            val articles = response.data.datas
            // 总页数
            val totalPage = response.data.pageCount

            // 计算上一页、下一页页码
            val prevPage = if (currentPage > 0) currentPage - 1 else null
            val nextPage = if (currentPage < totalPage - 1) currentPage + 1 else null

            // 返回加载结果
            LoadResult.Page(
                data = articles, // 当前页数据
                prevKey = prevPage, // 上一页页码
                nextKey = nextPage // 下一页页码
            )
        } catch (e: Exception) {
            // 加载失败返回错误
            LoadResult.Error(e)
        }
    }
}

关键点

  • load 函数是挂起函数,直接用协程请求网络
  • nextKeynull 时,代表没有更多数据
  • 异常必须捕获,返回 LoadResult.Error

步骤 4:创建 ViewModel 管理分页数据

ViewModel 负责创建 Pager,提供分页数据流,生命周期安全。

kotlin

kotlin 复制代码
class ArticleViewModel : ViewModel() {
    // 1. 配置分页参数
    private val pagingConfig = PagingConfig(
        pageSize = 20, // 每页加载20条数据
        prefetchDistance = 2, // 滑动到倒数第2条时预加载下一页
        enablePlaceholders = false, // 关闭占位(Compose 推荐关闭)
        initialLoadSize = 20 // 首次加载数量
    )

    // 2. 创建 Pager,输出 Flow<PagingData<Article>>
    val articlePagingFlow = Pager(pagingConfig) {
        ArticlePagingSource(RetrofitClient.api)
    }.flow // 转成 Flow
        .cachedIn(viewModelScope) // 缓存数据,屏幕旋转不丢失
}

PagingConfig 参数解释

  • pageSize:每页数据量,和接口保持一致
  • prefetchDistance:预加载阈值,数值越小,预加载越早
  • enablePlaceholders:Compose 中必须关闭,否则会报错

步骤 5:Compose 中展示分页列表 + 加载状态

这是最后一步!用 collectAsLazyPagingItems() 把 Flow 转成 Compose 可用的数据,配合 LazyColumn 展示。

完整 UI 代码:

kotlin

scss 复制代码
class MainActivity : ComponentActivity() {
    private val viewModel by viewModels<ArticleViewModel>()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    // 调用分页列表页面
                    ArticleListScreen(viewModel)
                }
            }
        }
    }
}

@Composable
fun ArticleListScreen(viewModel: ArticleViewModel) {
    // 1. 将 Flow 转为 LazyPagingItems
    val articleItems = viewModel.articlePagingFlow.collectAsLazyPagingItems()

    // 2. 下拉刷新状态
    val refreshState = rememberSwipeRefreshState(
        isRefreshing = articleItems.loadState.refresh is LoadState.Loading
    )

    Scaffold(topBar = {
        TopAppBar(title = { Text(text = "Paging3 + Compose 分页示例") })
    }) { padding ->
        // 下拉刷新
        SwipeRefresh(
            state = refreshState,
            onRefresh = { articleItems.refresh() }
        ) {
            LazyColumn(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(padding),
                contentPadding = PaddingValues(16.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                // 3. 展示列表数据
                items(articleItems) { article ->
                    article?.let {
                        ArticleItem(article = it)
                    }
                }

                // 4. 加载更多状态:加载中
                when {
                    articleItems.loadState.append is LoadState.Loading -> {
                        item {
                            LoadingItem(modifier = Modifier.fillMaxWidth())
                        }
                    }
                    // 加载更多失败
                    articleItems.loadState.append is LoadState.Error -> {
                        item {
                            ErrorItem(
                                modifier = Modifier.fillMaxWidth(),
                                onRetry = { articleItems.retry() }
                            )
                        }
                    }
                    // 没有更多数据
                    articleItems.loadState.append is LoadState.NotLoading &&
                            articleItems.itemCount < 20 -> { // 空数据
                        item {
                            EmptyItem(modifier = Modifier.fillMaxSize())
                        }
                    }
                }
            }
        }
    }
}

// 列表条目 UI
@Composable
fun ArticleItem(article: Article) {
    Card(
        modifier = Modifier.fillMaxWidth(),
        elevation = 4.dp
    ) {
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Text(text = article.title, style = MaterialTheme.typography.h6)
            Text(
                text = "作者:${article.shareUser}",
                modifier = Modifier.padding(top = 4.dp),
                color = Color.Gray
            )
        }
    }
}

// 加载中 UI
@Composable
fun LoadingItem(modifier: Modifier = Modifier) {
    Box(modifier = modifier.padding(16.dp), contentAlignment = Alignment.Center) {
        CircularProgressIndicator()
    }
}

// 加载失败 UI
@Composable
fun ErrorItem(modifier: Modifier = Modifier, onRetry: () -> Unit) {
    Box(modifier = modifier.padding(16.dp), contentAlignment = Alignment.Center) {
        Column(horizontalAlignment = Alignment.CenterHorizontally) {
            Text(text = "加载失败,点击重试")
            Button(onClick = onRetry, modifier = Modifier.padding(top = 8.dp)) {
                Text("重试")
            }
        }
    }
}

// 空数据 UI
@Composable
fun EmptyItem(modifier: Modifier = Modifier) {
    Box(modifier = modifier, contentAlignment = Alignment.Center) {
        Text(text = "暂无数据", style = MaterialTheme.typography.h6)
    }
}

四、核心功能解析(必看!)

1. Compose 中如何使用 Paging3?

  • collectAsLazyPagingItems() 扩展函数,将 Flow<PagingData<T>> 转为 LazyPagingItems<T>
  • 直接在 LazyColumnitems() 中使用,和普通列表用法完全一致

2. 加载状态监听(3 种状态)

Paging3 提供了统一的加载状态监听:

kotlin

go 复制代码
// 刷新状态(下拉刷新/首次加载)
articleItems.loadState.refresh

// 加载更多状态(滑动加载下一页)
articleItems.loadState.append

状态类型:

  • LoadState.Loading:加载中
  • LoadState.NotLoading:加载完成
  • LoadState.Error:加载失败

3. 常用操作

kotlin

scss 复制代码
// 下拉刷新
articleItems.refresh()

// 加载失败重试
articleItems.retry()

五、常见坑点总结(新手必避)

  1. enablePlaceholders 必须设为 false,Compose 不支持占位
  2. PagingSource 的 load 函数必须捕获所有异常,否则会崩溃
  3. 页码从 0 还是 1 开始,必须和接口保持一致
  4. 一定要用 .cachedIn(viewModelScope),避免旋转屏幕重新加载
  5. 预加载距离 prefetchDistance 不要太大,否则会提前加载多页

六、总结

Compose + Paging3 实现分页加载,只需要 4 个核心组件 + 5 步代码

  1. 定义数据实体
  2. 编写网络接口
  3. 实现 PagingSource(核心)
  4. ViewModel 中创建 Pager
  5. Compose 中用 LazyPagingItems 展示 + 状态监听
相关推荐
TO_ZRG1 小时前
Android Service基础
android
ECT-OS-JiuHuaShan2 小时前
功夫不负匠心人,渡劫代谢舞沧桑
android·开发语言·人工智能·算法·机器学习·kotlin·拓扑学
ZC跨境爬虫4 小时前
移动端爬虫工具Fiddler完整配置流程:PC+安卓模拟器全覆盖,零基础一次配置成功
android·前端·爬虫·测试工具·fiddler
巴德鸟4 小时前
DaVinci 常用技巧 关键帧 自动字幕 追踪 音频 冻结帧 快捷键 多轨道字幕 扩充边缘
android·编辑器·音视频·视频·davinci·davin
学习使我健康5 小时前
Android 广播介绍详情
android·开发语言·kotlin
dalancon5 小时前
AudioTrack Start 执行流程分析
android
众少成多积小致巨6 小时前
Android 初始化语言入门
android·linux·c++
Carson带你学Android6 小时前
谁才是地表最强 Android Agent 大模型?Google官方测评来了!
android·openai
followYouself6 小时前
ASM开源库实现函数耗时插桩
android·asm·asm插桩·字节码插桩