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

14 阅读5分钟

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

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

1. Paging3 是干嘛的?

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

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

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

表格

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

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

二、环境准备:添加依赖

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

gradle

// 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

// 文章实体类
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

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

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

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

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

// 刷新状态(下拉刷新/首次加载)
articleItems.loadState.refresh

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

状态类型:

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

3. 常用操作

kotlin

// 下拉刷新
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 展示 + 状态监听