Paging 3 分页库
Paging 库可以帮助您加载和显示来自本地存储或网络的大型数据集中的数据页面。这种方法可以让您的应用更有效地使用网络带宽和系统资源。Paging 3 库完全使用 Kotlin 协程重写,支持 Flow、RxJava 和 LiveData。
核心组件
Paging 库主要包含以下三个核心组件:
Repository 层
- PagingSource: 数据源的基础类。用于定义如何从数据源(如网络 API 或本地数据库)检索数据。
- RemoteMediator: 用于处理从网络加载数据并将其缓存到本地数据库(如 Room)的情况。
ViewModel 层
- Pager: 用于构建
PagingData实例的公共 API。它根据PagingConfig配置从PagingSource获取数据。 - PagingData: 包含分页数据的容器。它是一个不可变的数据流。
- Pager: 用于构建
UI 层
- PagingDataAdapter: 一个 RecyclerView Adapter,用于处理
PagingData并将其绑定到 UI。它会自动处理 DiffUtil 来高效更新列表。
- PagingDataAdapter: 一个 RecyclerView Adapter,用于处理
1. 添加依赖
kotlin
[versions]
paging = "3.3.0"
[libraries]
androidx-paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" }
androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" }kotlin
dependencies {
implementation(libs.androidx.paging.runtime)
// 如果使用 Compose
implementation(libs.androidx.paging.compose)
}2. 定义 PagingSource
PagingSource<Key, Value> 需要定义两个泛型:
- Key: 用于加载数据的标识(例如页码)。
- Value: 加载的数据类型。
你需要实现两个方法:load() 和 getRefreshKey()。
kotlin
import androidx.paging.PagingSource
import androidx.paging.PagingState
class ExamplePagingSource(
private val backend: ExampleBackendService
) : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
return try {
// 获取当前的页码,如果为空则默认为第 1 页
val nextPageNumber = params.key ?: 1
// 从后端获取数据
val response = backend.searchUsers(page = nextPageNumber)
LoadResult.Page(
data = response.users,
// 如果当前是第一页,prevKey 为 null
prevKey = if (nextPageNumber == 1) null else nextPageNumber - 1,
// 如果没有更多数据,nextKey 为 null
nextKey = if (response.users.isEmpty()) null else nextPageNumber + 1
)
} catch (e: Exception) {
// 处理错误,返回 Error
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
// 当数据刷新时(例如调用 adapter.refresh()),提供一个新的 Key
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}3. 在 Repository 中创建 Pager
在 Repository 中,你需要创建 Pager 实例并配置 PagingConfig。
kotlin
class UserRepository(private val backend: ExampleBackendService) {
fun getUserStream(): Flow<PagingData<User>> {
return Pager(
config = PagingConfig(
pageSize = 20, // 每页加载的数量
enablePlaceholders = false // 是否启用占位符
),
pagingSourceFactory = { ExamplePagingSource(backend) }
).flow
}
}4. 在 ViewModel 中使用
在 ViewModel 中,可以使用 .cachedIn(viewModelScope) 来缓存数据流,确保配置更改(如屏幕旋转)后数据仍然存在。
kotlin
class UserViewModel(private val repository: UserRepository) : ViewModel() {
val userPagingFlow: Flow<PagingData<User>> = repository.getUserStream()
.cachedIn(viewModelScope)
}5. UI 层:PagingDataAdapter
创建一个继承自 PagingDataAdapter 的 Adapter。
kotlin
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
// ... ViewBinding imports
class UserAdapter : PagingDataAdapter<User, UserViewHolder>(USER_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
// 使用 ViewBinding 或 LayoutInflater 创建 ViewHolder
val binding = ItemUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return UserViewHolder(binding)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val user = getItem(position)
if (user != null) {
holder.bind(user)
}
}
companion object {
private val USER_COMPARATOR = object : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean =
oldItem == newItem
}
}
}6. 在 Activity/Fragment 中绑定
kotlin
class UserActivity : AppCompatActivity() {
// ... setup binding and viewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val adapter = UserAdapter()
binding.recyclerView.adapter = adapter
// 收集 Flow 数据并提交给 Adapter
lifecycleScope.launch {
viewModel.userPagingFlow.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
}
}7. 显示加载状态 (LoadStateAdapter)
Paging 3 提供了 LoadState 来表示加载状态(加载中、错误、完成)。你可以使用 LoadStateAdapter 在列表的头部或尾部显示加载进度条或重试按钮。
kotlin
class ExampleLoadStateAdapter(private val retry: () -> Unit) :
LoadStateAdapter<LoadStateViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder {
return LoadStateViewHolder.create(parent, retry)
}
override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) {
holder.bind(loadState)
}
}在 Activity 中连接:
kotlin
binding.recyclerView.adapter = adapter.withLoadStateFooter(
footer = ExampleLoadStateAdapter { adapter.retry() }
)
// 或者同时添加头部和尾部
// adapter.withLoadStateHeaderAndFooter(...)监听加载状态以处理 UI(例如初始加载时的全屏进度条):
kotlin
lifecycleScope.launch {
adapter.loadStateFlow.collect { loadState ->
val isListEmpty = loadState.refresh is LoadState.NotLoading && adapter.itemCount == 0
// 显示/隐藏 空视图
binding.emptyView.isVisible = isListEmpty
// 显示/隐藏 进度条
binding.progressBar.isVisible = loadState.source.refresh is LoadState.Loading
// 处理错误
val errorState = loadState.source.append as? LoadState.Error
?: loadState.source.prepend as? LoadState.Error
?: loadState.append as? LoadState.Error
?: loadState.prepend as? LoadState.Error
errorState?.let {
Toast.makeText(this@UserActivity, "\${it.error}", Toast.LENGTH_LONG).show()
}
}
}
## 8. 在 Compose 中使用
Paging 3 对 Compose 的支持非常丝滑。你需要使用 `androidx.paging:paging-compose` 库。
```kotlin
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey
@Composable
fun UserListScreen(viewModel: UserViewModel) {
// 1. 将 Flow<PagingData> 转换为 LazyPagingItems
val userItems = viewModel.userPagingFlow.collectAsLazyPagingItems()
LazyColumn {
// 2. 使用 items 扩展函数
items(
count = userItems.itemCount,
key = userItems.itemKey { it.id }, // 优化滚动性能
contentType = userItems.itemContentType { "user" }
) { index ->
val user = userItems[index]
if (user != null) {
UserRow(user)
} else {
// Paging 3 支持占位符 (Placeholders),如果开启的话这里会是 null
UserPlaceholder()
}
}
// 3. 处理加载状态 (底部加载更多)
item {
if (userItems.loadState.append is LoadState.Loading) {
CircularProgressIndicator()
}
}
}
}