You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
    }
}

解决方案

问题根源

  1. 占位符导致初始位置计算错误:PagingConfig中启用了enablePlaceholders = true,首次加载时数据库为空,Paging会生成pageSize数量的占位项。真实数据加载完成后,占位项被替换,RecyclerView的滚动位置被错误计算到占位项末尾。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 08:21:57