Paging 3 分页加载:从配置到实战
一、什么是 Paging 3
Paging 3 是 Jetpack 组件库中专门用于分页加载的库,可以高效处理大数据集,避免一次性加载过多数据导致内存溢出或界面卡顿。
核心优势:
- 自动管理分页逻辑
- 内置加载状态(加载中、成功、失败)
- 支持网络 + 本地双层缓存
- 与 RecyclerView 无缝集成
二、核心组件
1. PagingSource
数据源抽象,负责从网络或数据库分页拉取数据。
class ArticlePagingSource(
private val api: ApiService
) : PagingSource<Int, Article>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
return try {
val page = params.key ?: 1
val response = api.getArticles(page, params.loadSize)
LoadResult.Page(
data = response.data,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.data.isEmpty()) null else page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
2. Pager
配置分页参数,返回 Flow。
class ArticleRepository(private val api: ApiService) {
fun getArticleStream(): Flow<PagingData<Article>> {
return Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
initialLoadSize = 20
),
pagingSourceFactory = { ArticlePagingSource(api) }
).flow
}
}
3. PagingDataAdapter
RecyclerView 的适配器,自动处理数据更新和差异对比。
class ArticleAdapter : PagingDataAdapter<Article, ArticleAdapter.ViewHolder>(DiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemArticleBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val article = getItem(position)
article?.let { holder.bind(it) }
}
class ViewHolder(private val binding: ItemArticleBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(article: Article) {
binding.title.text = article.title
binding.author.text = article.author
}
}
companion object DiffCallback : DiffUtil.ItemCallback<Article>() {
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
}
三、ViewModel 集成
class ArticleViewModel(
private val repository: ArticleRepository
) : ViewModel() {
val articles: Flow<PagingData<Article>> = repository.getArticleStream()
.cachedIn(viewModelScope)
}
cachedIn(viewModelScope) 确保在配置变更(如旋转屏幕)时缓存数据,避免重复请求。
四、Activity/Fragment 使用
class ArticleActivity : AppCompatActivity() {
private lateinit var binding: ActivityArticleBinding
private val viewModel: ArticleViewModel by viewModels()
private val adapter = ArticleAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityArticleBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.recyclerView.adapter = adapter
lifecycleScope.launch {
viewModel.articles.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
}
}
五、加载状态处理
Paging 3 提供 LoadStateAdapter 显示加载进度和错误提示。
class LoadStateAdapter(
private val retry: () -> Unit
) : LoadStateAdapter<LoadStateAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {
val binding = ItemLoadStateBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ViewHolder(binding, retry)
}
override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {
holder.bind(loadState)
}
class ViewHolder(
private val binding: ItemLoadStateBinding,
private val retry: () -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(loadState: LoadState) {
binding.progressBar.isVisible = loadState is LoadState.Loading
binding.retryButton.isVisible = loadState is LoadState.Error
binding.errorText.isVisible = loadState is LoadState.Error
binding.retryButton.setOnClickListener { retry() }
}
}
}
在 Activity 中挂载:
binding.recyclerView.adapter = adapter.withLoadStateFooter(
footer = LoadStateAdapter { adapter.retry() }
)
六、刷新与重试
// 下拉刷新
binding.swipeRefresh.setOnRefreshListener {
adapter.refresh()
}
// 监听加载状态
lifecycleScope.launch {
adapter.loadStateFlow.collectLatest { loadStates ->
binding.swipeRefresh.isRefreshing = loadStates.refresh is LoadState.Loading
}
}
七、Room 本地缓存 + RemoteMediator
结合 Room 数据库实现离线缓存和网络加载的混合方案。
@OptIn(ExperimentalPagingApi::class)
class ArticleRemoteMediator(
private val api: ApiService,
private val database: AppDatabase
) : RemoteMediator<Int, Article>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Article>
): MediatorResult {
return try {
val page = when (loadType) {
LoadType.REFRESH -> 1
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull()
?: return MediatorResult.Success(endOfPaginationReached = true)
lastItem.page + 1
}
}
val response = api.getArticles(page, state.config.pageSize)
database.withTransaction {
if (loadType == LoadType.REFRESH) {
database.articleDao().clearAll()
}
database.articleDao().insertAll(response.data)
}
MediatorResult.Success(endOfPaginationReached = response.data.isEmpty())
} catch (e: Exception) {
MediatorResult.Error(e)
}
}
}
Pager 配置:
@OptIn(ExperimentalPagingApi::class)
fun getArticleStream(): Flow<PagingData<Article>> {
return Pager(
config = PagingConfig(pageSize = 20),
remoteMediator = ArticleRemoteMediator(api, database),
pagingSourceFactory = { database.articleDao().pagingSource() }
).flow
}
八、总结
| 场景 | 方案 |
|---|---|
| 纯网络分页 | PagingSource + Pager |
| 网络 + 本地缓存 | RemoteMediator + Room |
| 加载状态展示 | LoadStateAdapter |
| 下拉刷新 | adapter.refresh() |
Paging 3 让分页加载变得简单高效,适合处理列表、信息流等大数据场景。