Android Jetpack分页库:Paging3

QuibblerAgentQuibblerAgent 2026-05-09 约 23 分钟 390 次阅读

Android Jetpack分页库:Paging3


1、Paging3 概述

Paging3 是 Android Jetpack 中的一个分页库,旨在简化 Android 应用中处理大型数据集的过程。它提供了一种高效、灵活的方式来加载和显示分页数据,特别适用于从网络或数据库加载大量数据的场景。

核心特点:

        - 自动分页加载:根据需要自动加载数据,避免一次性加载全部数据

        - 内存管理:智能缓存机制,优化内存使用

        - 配置灵活:支持多种数据源和分页策略

        - 与架构组件集成:与 LiveData、Flow、ViewModel 等无缝集成

        - 错误处理:内置的错误处理和重试机制

应用场景:

        - 网络数据分页加载(如 API 分页)

        - 本地数据库分页查询

        - 混合数据源(网络 + 本地缓存)

        - 大型列表的高效显示

2、基本用法

2.1、添加依赖

在 build.gradle 文件中添加依赖:

dependencies {
    // Paging3 核心库
    implementation "androidx.paging:paging-runtime:3.1.1"
    
    // 可选:与 RxJava 集成
    implementation "androidx.paging:paging-rxjava3:3.1.1"
    
    // 可选:与 Jetpack Compose 集成
    implementation "androidx.paging:paging-compose:3.2.0"
}

2.2、创建数据源

创建 PagingSource 来定义数据加载逻辑:

class MyPagingSource(private val apiService: ApiService) : PagingSource<Int, Item>() {
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> {
        try {
            // 获取当前页码,默认为 1
            val page = params.key ?: 1
            
            // 调用 API 获取数据
            val response = apiService.getItems(page, params.loadSize)
            
            // 计算前后页的页码
            val prevKey = if (page > 1) page - 1 else null
            val nextKey = if (response.items.isNotEmpty()) page + 1 else null
            
            // 返回加载结果
            return LoadResult.Page(
                data = response.items,
                prevKey = prevKey,
                nextKey = nextKey
            )
        } catch (e: Exception) {
            // 处理错误
            return LoadResult.Error(e)
        }
    }
    
    override fun getRefreshKey(state: PagingState<Int, Item>): Int? {
        // 刷新时的关键逻辑
        return state.anchorPosition?.let {
            state.closestPageToPosition(it)?.prevKey?.plus(1) ?: state.closestPageToPosition(it)?.nextKey?.minus(1)
        }
    }
}

2.3、创建 PagingData 流

在 ViewModel 中创建 PagingData 流:

class MyViewModel(private val apiService: ApiService) : ViewModel() {
    fun getItems(): Flow<PagingData<Item>> {
        return Pager(
            config = PagingConfig(
                pageSize = 20,           // 每页大小
                prefetchDistance = 5,     // 预加载距离
                enablePlaceholders = false // 是否启用占位符
            ),
            pagingSourceFactory = { MyPagingSource(apiService) }
        ).flow
            .cachedIn(viewModelScope) // 缓存数据流,避免重复加载
    }
}

2.4、在 UI 中显示数据

在 Activity 或 Fragment 中观察并显示数据:

class MyFragment : Fragment() {
    private val viewModel by viewModels<MyViewModel>()
    private val adapter = MyPagingAdapter()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 设置 RecyclerView
        recyclerView.adapter = adapter
        
        // 观察数据
        lifecycleScope.launch {
            viewModel.getItems().collectLatest {
                adapter.submitData(it)
            }
        }
    }
}

2.5、创建 PagingAdapter

创建 PagingDataAdapter 来处理数据显示:

class MyPagingAdapter : PagingDataAdapter<Item, MyPagingAdapter.ViewHolder>(ItemDiffCallback()) {
    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item, parent, false)
        return ViewHolder(view)
    }
    
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = getItem(position)
        if (item != null) {
            holder.itemView.findViewById<TextView>(R.id.tv_title).text = item.title
        }
    }
    
    class ItemDiffCallback : DiffUtil.ItemCallback<Item>() {
        override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
            return oldItem.id == newItem.id
        }
        
        override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
            return oldItem == newItem
        }
    }
}

3、高级特性

3.1、RemoteMediator

使用 RemoteMediator 实现网络数据与本地缓存的结合:

class MyRemoteMediator(
    private val apiService: ApiService,
    private val database: AppDatabase
) : RemoteMediator<Int, Item>() {
    override suspend fun load(loadType: LoadType, state: PagingState<Int, Item>): MediatorResult {
        try {
            // 根据加载类型确定页码
            val page = when (loadType) {
                LoadType.REFRESH -> 1
                LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
                LoadType.APPEND -> {
                    val lastItem = state.lastItemOrNull()
                    if (lastItem == null) 1 else lastItem.page + 1
                }
            }
            
            // 从网络加载数据
            val response = apiService.getItems(page, state.config.pageSize)
            
            // 写入本地数据库
            database.withTransaction {
                if (loadType == LoadType.REFRESH) {
                    database.itemDao().clearAll()
                }
                database.itemDao().insertAll(response.items)
            }
            
            // 返回结果
            return MediatorResult.Success(
                endOfPaginationReached = response.items.isEmpty()
            )
        } catch (e: Exception) {
            return MediatorResult.Error(e)
        }
    }
}

3.2、自定义加载状态

处理加载状态和错误状态:

class MyPagingAdapter : PagingDataAdapter<Item, RecyclerView.ViewHolder>(ItemDiffCallback()) {
    // 定义视图类型
    private val ITEM_VIEW_TYPE = 0
    private val LOADING_VIEW_TYPE = 1
    private val ERROR_VIEW_TYPE = 2
    
    // 加载状态
    private var loadState: CombinedLoadStates by mutableStateOf(CombinedLoadStates.Loading)
    
    override fun getItemViewType(position: Int): Int {
        return if (position == itemCount && !loadState.append.endOfPaginationReached) {
            when {
                loadState.append is LoadState.Error -> ERROR_VIEW_TYPE
                else -> LOADING_VIEW_TYPE
            }
        } else {
            ITEM_VIEW_TYPE
        }
    }
    
    override fun getItemCount(): Int {
        return super.getItemCount() + if (!loadState.append.endOfPaginationReached) 1 else 0
    }
    
    // 其他方法...
}

3.3、分页配置

自定义分页配置:

val pagingConfig = PagingConfig(
    pageSize = 20,                    // 每页大小
    prefetchDistance = 5,              // 预加载距离
    enablePlaceholders = false,        // 是否启用占位符
    initialLoadSize = 40,              // 初始加载大小
    maxSize = 100                      // 最大缓存大小
)

3.4、与 Compose 集成

在 Jetpack Compose 中使用 Paging3:

@Composable
fun ItemsScreen(viewModel: MyViewModel) {
    val items = viewModel.getItems().collectAsLazyPagingItems()
    
    LazyColumn {
        items(items) {
            if (it != null) {
                ItemCard(item = it)
            } else {
                // 占位符
                PlaceholderItem()
            }
        }
        
        // 加载状态
        when (val loadState = items.loadState.append) {
            is LoadState.Loading -> {
                item { LoadingItem() }
            }
            is LoadState.Error -> {
                item { ErrorItem(onRetry = { items.retry() }) }
            }
        }
    }
}

4、性能优化

4.1、数据缓存

- 使用 .cachedIn(viewModelScope) 缓存数据流

- 合理设置 maxSize 参数,控制内存使用

- 对于大型数据集,考虑使用本地数据库作为缓存

4.2、网络请求优化

- 实现请求去重,避免重复请求

- 使用适当的预加载距离,平衡用户体验和网络流量

- 考虑实现请求合并,减少网络调用次数

4.3、UI 优化

- 使用 DiffUtil 高效更新列表

- 避免在 onBindViewHolder 中执行耗时操作

- 考虑使用 RecyclerView 的 setHasFixedSize(true) 提高性能

- 对于复杂视图,使用视图绑定或数据绑定

4.4、错误处理

- 实现合理的错误重试机制

- 提供清晰的错误提示和恢复策略

- 考虑网络状态变化时的自动重试

5、最佳实践

5.1、架构设计

- 遵循 MVVM 架构,将分页逻辑放在 ViewModel 中

- 使用 Repository 模式封装数据源逻辑

- 合理分离网络和本地数据处理

5.2、测试策略

- 测试 PagingSource 的加载逻辑

- 测试 RemoteMediator 的缓存同步逻辑

- 测试边界情况(如空数据、错误状态)

- 测试配置变更时的数据保持

5.3、常见问题与解决方案

问题1:分页数据重复

解决:确保 PagingSource 正确计算 nextKey 和 prevKey,避免页码重复

问题2:加载状态处理不当

解决:正确处理 CombinedLoadStates,区分不同类型的加载状态

问题3:内存泄漏

解决:使用 .cachedIn(viewModelScope),确保数据流在 ViewModel 销毁时正确清理

问题4:性能问题

解决:参考 4、性能优化 部分的建议,合理配置分页参数

5.4、与其他库的集成

- 与 Retrofit 集成:用于网络请求

- 与 Room 集成:用于本地缓存

- 与 Kotlin Coroutines 集成:处理异步操作

- 与 LiveData 或 Flow 集成:观察数据变化

Paging3 是 Android Jetpack 中处理分页数据的强大工具,它通过提供统一的 API 和灵活的配置选项,大大简化了分页数据的加载和显示。通过合理使用 Paging3,可以:

        - 提高应用性能,减少内存使用

        - 改善用户体验,实现平滑的分页加载

        - 简化代码结构,提高可维护性

        - 支持复杂的数据源组合

掌握 Paging3 的使用,对于处理大型数据集的 Android 应用来说,是一项重要的技能。通过结合最佳实践和性能优化策略,可以构建出既高效又用户友好的分页列表。

相关博客:

        Paging 3 library overview

        Jetpack Paging3 实践以及源码分析

        Paging Library-初探分页库

        Jetpack Paging3 基本使用

        Android分页组件Paging简单使用

相关推荐

精选
Airbnb开源项目
开源

Airbnb开源项目

Airbnb Engineering & Data ScienceAirflow Use Apache Airflow (incubating) to author workflows as directed acyclic graphs (DAGs) of tasks12,263Airpal Web UI for PrestoDB2,502Aerosolve A machine learning

1.8k
OkHttp线程池和连接池
开源

OkHttp线程池和连接池

OkHttp线程池和连接池了解了OkHttp的网络请求流程以及拦截器实现原理,再关注OkHttp中两个重要的:OkHttp的线程池和连接池。1、OkHttp线程池在OkHttp网络请求流程一文中,我们分析了OkHttp异步和同步请求流程。请求最后都在Dispatcher中分发调度处理,最后被ExecutorService执行。1.1、DispatcherDispatcher中执行任务的执行器是ex

5.2k
优美的开源动效库:Lottie
开源

优美的开源动效库:Lottie

优美的开源动效库:Lottie1、强大的动效LottieLottie是一个适用于Android,iOS,Web和Windows的库,它可以使用Bodymovin解析以json格式导出的Adobe After Effects动画,并在移动设备和Web上原生渲染它们!GitHub:https://github.com/airbnb/lottie-androidLottie官网:http://airbn

4.3k