Android Paging Library 3首次加载后列表跳转问题求助
问题
我正在开发一款展示所有GitHub用户列表的应用,使用Retrofit、Paging Library 3 + Room技术栈。整体功能正常,但首次加载数据后,列表会跳转到pageSize对应的位置,重新进入应用时不会出现该问题。以下是相关代码:
Dao
@Dao interface UsersDao { @Query("SELECT * FROM ${UserListItem.TABLE_NAME}") fun getUsers(): PagingSource<Int, UserListItem> }
ViewModel
private const val DEFAULT_PAGE_SIZE = 30 class UserListViewModel( private val dao: UsersDao, remoteMediator: UserListRemoteMediator, ) : ViewModel() { @OptIn(ExperimentalPagingApi::class) private val pager = Pager( config = PagingConfig( pageSize = DEFAULT_PAGE_SIZE, enablePlaceholders = true, maxSize = DEFAULT_PAGE_SIZE * 3, ), remoteMediator = remoteMediator, pagingSourceFactory = { dao.getUsers() }, ) val users: Flow<PagingData<UserListItem>> = pager.flow.cachedIn(viewModelScope) }
RemoteMediator
@OptIn(ExperimentalPagingApi::class) class UserListRemoteMediator( private val database: UsersDatabase, private val backend: UsersApi, private val network: Utils.Network, ) : RemoteMediator<Int, UserListItem>() { private val dao = database.getUsersDao() override suspend fun initialize(): InitializeAction { return if (network.isInternetConnected) InitializeAction.LAUNCH_INITIAL_REFRESH else InitializeAction.SKIP_INITIAL_REFRESH } override suspend fun load( loadType: LoadType, state: PagingState<Int, UserListItem> ): MediatorResult { try { val maxId = when (loadType) { LoadType.REFRESH -> { null } LoadType.PREPEND -> { return MediatorResult.Success(endOfPaginationReached = true) } LoadType.APPEND -> { dao.getMaxId() } } val response = backend.getUsers( after = maxId, amount = state.config.pageSize ) if (response.isSuccessful) { return handleSuccessState(loadType, response) } if (response.isRequestsLimitExceeded) { return handleRequestLimitExceededState(response) } return errorWithMessage(message = "Unhandled response code: ${response.code()}") } catch (e: IOException) { return MediatorResult.Error(e) } catch (e: HttpException) { return MediatorResult.Error(e) } } }
Fragment中Adapter设置方法
private fun setUpAdapter(binding: FragmentUserListBinding, adapter: UserPagingDataAdapter) { binding.list.adapter = adapter.withLoadStateHeaderAndFooter( header = LoadStateAdapter(adapter::retry), footer = LoadStateAdapter(adapter::retry) ) viewLifecycleOwner.lifecycleScope.launch { viewModel.users.collectLatest { adapter.submitData(it) } } }
Fragment布局
<?xml version="1.0" encoding="utf-8"?> <androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/user_list_swipe_refresh_layout" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/list" android:layout_width="match_parent" android:layout_height="wrap_content" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:context=".view.userlist.UserListFragment" /> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
LoadStateAdapter
class LoadStateAdapter( private val retry: () -> Unit, ) : LoadStateAdapter<LoadStateViewHolder>() { override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) { holder.bind(loadState) } override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder { val layoutInflater = LayoutInflater.from(parent.context) val binding = LoadStateItemBinding.inflate(layoutInflater, parent, false) return LoadStateViewHolder(binding, retry) } }
LoadStateViewHolder
class LoadStateViewHolder( private val binding: LoadStateItemBinding, retry: () -> Unit, ) : RecyclerView.ViewHolder(binding.root) { init { binding.retryButton.setOnClickListener { retry() } } fun bind(loadState: LoadState) { if (loadState is LoadState.Error) { binding.errorMessage.text = loadState.error.localizedMessage } binding.errorMessage.isVisible = loadState is LoadState.Error binding.progressBar.isVisible = loadState is LoadState.Loading binding.retryButton.isVisible = loadState is LoadState.Error } }
解决方案
问题根源
- 占位符导致初始位置计算错误:
PagingConfig中启用了enablePlaceholders = true,首次加载时数据库为空,Paging会生成pageSize数量的占位项。真实数据加载完成后,占位项被替换,RecyclerView的滚动位置被错误计算到占位项末尾。 - RecyclerView高度设置不合理:布局中RecyclerView高度为
wrap_content,初始占位项会让RecyclerView计算出错误高度,真实数据加载后高度变化触发滚动位置偏移。
修复步骤
1. 关闭Paging占位符(优先方案)
修改ViewModel中的PagingConfig,将enablePlaceholders设为false:
@OptIn(ExperimentalPagingApi::class) private val pager = Pager( config = PagingConfig( pageSize = DEFAULT_PAGE_SIZE, enablePlaceholders = false, // 关闭占位符 maxSize = DEFAULT_PAGE_SIZE * 3, ), remoteMediator = remoteMediator, pagingSourceFactory = { dao.getUsers() }, )
关闭占位符后,Paging仅在有真实数据时渲染列表项,避免占位项导致的位置偏移。
2. 修正RecyclerView高度
修改Fragment布局中RecyclerView的高度为match_parent:
<androidx.recyclerview.widget.RecyclerView android:id="@+id/list" android:layout_width="match_parent" android:layout_height="match_parent" <!-- 改为match_parent --> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:context=".view.userlist.UserListFragment" />
固定RecyclerView高度为父容器高度,避免占位项与真实数据的高度差异引发滚动偏移。
3. 可选:优化占位符(若需保留)
如果必须保留占位符,需让PagingSource返回正确的总数据量:
- 在
UsersDao中添加查询总条数的方法:@Query("SELECT COUNT(*) FROM ${UserListItem.TABLE_NAME}") suspend fun getUserCount(): Int - 自定义
PagingSource,在load方法中返回正确的itemCount(需通过RemoteMediator获取GitHub用户总条数并存储到数据库,让Room的PagingSource能正确计算总数量)。
验证效果
完成上述修改后,首次加载数据时列表会保持在顶部位置,不会出现自动跳转到pageSize位置的问题。
内容的提问来源于stack exchange,提问作者Empty
相关产品推荐
相关产品推荐

