Paging分页加载 #

一、Paging概述 #

Paging是Jetpack提供的分页加载组件,用于高效加载和显示大量数据。

1.1 Paging的优势 #

  • 内存高效
  • 自动加载更多
  • 支持多种数据源
  • 支持列表更新

1.2 添加依赖 #

kotlin
dependencies {
    implementation("androidx.paging:paging-runtime-ktx:3.2.1")
}

二、PagingSource #

2.1 创建PagingSource #

kotlin
class UserPagingSource(
    private val apiService: ApiService
) : PagingSource<Int, User>() {
    
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
        return try {
            val page = params.key ?: 1
            val response = apiService.getUsers(page, params.loadSize)
            
            LoadResult.Page(
                data = response.users,
                prevKey = if (page == 1) null else page - 1,
                nextKey = if (response.users.isEmpty()) null else page + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
    
    override fun getRefreshKey(state: PagingState<Int, User>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }
}

2.2 Room PagingSource #

kotlin
@Dao
interface UserDao {
    @Query("SELECT * FROM users ORDER BY name ASC")
    fun getPagingSource(): PagingSource<Int, User>
}

三、Pager #

3.1 创建Pager #

kotlin
class UserRepository(private val apiService: ApiService) {
    
    fun getUsers(): Flow<PagingData<User>> {
        return Pager(
            config = PagingConfig(
                pageSize = 20,
                prefetchDistance = 10,
                enablePlaceholders = false
            ),
            pagingSourceFactory = { UserPagingSource(apiService) }
        ).flow
    }
}

3.2 PagingConfig参数 #

参数 说明
pageSize 每页数据量
prefetchDistance 预加载距离
enablePlaceholders 是否启用占位符
initialLoadSize 初始加载数量
maxSize 最大缓存数量

四、PagingDataAdapter #

4.1 创建Adapter #

kotlin
class UserAdapter(
    private val onClick: (User) -> Unit
) : PagingDataAdapter<User, UserViewHolder>(DiffCallback) {
    
    companion object DiffCallback : DiffUtil.ItemCallback<User>() {
        override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
            return oldItem.id == newItem.id
        }
        
        override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
            return oldItem == newItem
        }
    }
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val binding = ItemUserBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return UserViewHolder(binding)
    }
    
    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        val user = getItem(position)
        user?.let { holder.bind(it, onClick) }
    }
}

class UserViewHolder(
    private val binding: ItemUserBinding
) : RecyclerView.ViewHolder(binding.root) {
    
    fun bind(user: User, onClick: (User) -> Unit) {
        binding.name.text = user.name
        binding.root.setOnClickListener { onClick(user) }
    }
}

4.2 在Activity中使用 #

kotlin
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    
    private val viewModel: MainViewModel by viewModels()
    private lateinit var adapter: UserAdapter
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        adapter = UserAdapter { user ->
            // 点击事件
        }
        
        binding.recyclerView.adapter = adapter
        
        lifecycleScope.launch {
            viewModel.users.collectLatest { pagingData ->
                adapter.submitData(pagingData)
            }
        }
    }
}

五、ViewModel配置 #

kotlin
@HiltViewModel
class MainViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {
    
    val users: Flow<PagingData<User>> = repository.getUsers()
        .cachedIn(viewModelScope)
}

六、加载状态 #

6.1 添加加载状态Footer #

kotlin
class LoadStateAdapter(
    private val retry: () -> Unit
) : RecyclerView.Adapter<LoadStateViewHolder>() {
    
    private var loadState: LoadState = LoadState.NotLoading(false)
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LoadStateViewHolder {
        val binding = ItemLoadStateBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return LoadStateViewHolder(binding, retry)
    }
    
    override fun onBindViewHolder(holder: LoadStateViewHolder, position: Int) {
        holder.bind(loadState)
    }
    
    override fun getItemCount(): Int = 1
    
    fun setLoadState(state: LoadState) {
        loadState = state
        notifyDataSetChanged()
    }
}

class LoadStateViewHolder(
    private val binding: ItemLoadStateBinding,
    private val retry: () -> Unit
) : RecyclerView.ViewHolder(binding.root) {
    
    init {
        binding.retryButton.setOnClickListener { retry() }
    }
    
    fun bind(loadState: LoadState) {
        binding.progressBar.isVisible = loadState is LoadState.Loading
        binding.retryButton.isVisible = loadState is LoadState.Error
        binding.errorMsg.isVisible = loadState is LoadState.Error
        
        if (loadState is LoadState.Error) {
            binding.errorMsg.text = loadState.error.localizedMessage
        }
    }
}

6.2 使用withLoadStateFooter #

kotlin
binding.recyclerView.adapter = adapter.withLoadStateFooter(
    footer = LoadStateAdapter { adapter.retry() }
)

6.3 监听加载状态 #

kotlin
adapter.addLoadStateListener { loadState ->
    binding.progressBar.isVisible = loadState.refresh is LoadState.Loading
    binding.retryButton.isVisible = loadState.refresh is LoadState.Error
    
    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, it.error.message, Toast.LENGTH_LONG).show()
    }
}

七、数据刷新 #

7.1 刷新数据 #

kotlin
// 刷新数据
adapter.refresh()

// 重试失败
adapter.retry()

7.2 过滤和搜索 #

kotlin
class MainViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {
    
    private val _searchQuery = MutableStateFlow("")
    val searchQuery: StateFlow<String> = _searchQuery
    
    val users: Flow<PagingData<User>> = searchQuery
        .debounce(300)
        .flatMapLatest { query ->
            repository.searchUsers(query)
        }
        .cachedIn(viewModelScope)
    
    fun setSearchQuery(query: String) {
        _searchQuery.value = query
    }
}

八、RemoteMediator #

8.1 创建RemoteMediator #

kotlin
@OptIn(ExperimentalPagingApi::class)
class UserRemoteMediator(
    private val apiService: ApiService,
    private val database: AppDatabase
) : RemoteMediator<Int, User>() {
    
    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, User>
    ): MediatorResult {
        return 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.id / state.config.pageSize) + 1
                    }
                }
            }
            
            val response = apiService.getUsers(page)
            
            database.withTransaction {
                if (loadType == LoadType.REFRESH) {
                    database.userDao().clearAll()
                }
                database.userDao().insertAll(response.users)
            }
            
            MediatorResult.Success(
                endOfPaginationReached = response.users.isEmpty()
            )
        } catch (e: IOException) {
            MediatorResult.Error(e)
        } catch (e: HttpException) {
            MediatorResult.Error(e)
        }
    }
}

8.2 使用RemoteMediator #

kotlin
@OptIn(ExperimentalPagingApi::class)
fun getUsers(): Flow<PagingData<User>> {
    return Pager(
        config = PagingConfig(pageSize = 20),
        remoteMediator = UserRemoteMediator(apiService, database),
        pagingSourceFactory = { database.userDao().getPagingSource() }
    ).flow
}

九、总结 #

本章详细介绍了Paging分页加载:

  1. Paging的基本概念
  2. PagingSource的创建
  3. Pager的配置
  4. PagingDataAdapter的使用
  5. 加载状态处理
  6. 数据刷新
  7. RemoteMediator的使用

Paging是处理大数据集分页加载的最佳方案,可以高效地加载和显示大量数据。

最后更新:2026-03-26