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
// 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函数是挂起函数,直接用协程请求网络nextKey为null时,代表没有更多数据- 异常必须捕获,返回
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> - 直接在
LazyColumn的items()中使用,和普通列表用法完全一致
2. 加载状态监听(3 种状态)
Paging3 提供了统一的加载状态监听:
kotlin
// 刷新状态(下拉刷新/首次加载)
articleItems.loadState.refresh
// 加载更多状态(滑动加载下一页)
articleItems.loadState.append
状态类型:
LoadState.Loading:加载中LoadState.NotLoading:加载完成LoadState.Error:加载失败
3. 常用操作
kotlin
// 下拉刷新
articleItems.refresh()
// 加载失败重试
articleItems.retry()
五、常见坑点总结(新手必避)
enablePlaceholders必须设为 false,Compose 不支持占位- PagingSource 的
load函数必须捕获所有异常,否则会崩溃 - 页码从 0 还是 1 开始,必须和接口保持一致
- 一定要用
.cachedIn(viewModelScope),避免旋转屏幕重新加载 - 预加载距离
prefetchDistance不要太大,否则会提前加载多页
六、总结
Compose + Paging3 实现分页加载,只需要 4 个核心组件 + 5 步代码:
- 定义数据实体
- 编写网络接口
- 实现 PagingSource(核心)
- ViewModel 中创建 Pager
- Compose 中用 LazyPagingItems 展示 + 状态监听