使用Pager3 RemoteMediator+Room分页时双标签页刷新丢数据问题
双标签页Pager3 + Room分页数据丢失问题修复方案
问题背景
我的Android应用包含两个标签页:好友动态页和推荐内容页,两者均使用Pager3的RemoteMediator结合Room的PagingSource实现分页,共享同一张数据库表,通过source字段(feed/discover)区分不同标签页的数据,UI基于Jetpack Compose开发。
当前遇到的问题:当同时刷新两个标签页并切换时,其中一个或两个标签页的分页数据会显示0条,直到两者都完成加载/刷新流程。
相关代码片段
PagerFactory实现
@OptIn(ExperimentalPagingApi::class) fun getFeedPager(currentUserId: String): Flow<PagingData<Post>> { return Pager( config = PagingConfig( pageSize = FEED_PAGE_SIZE, prefetchDistance = FEED_PAGE_SIZE, initialLoadSize = FEED_PAGE_SIZE ), pagingSourceFactory = { database.postDao().getPostsPagingSource(PostSource.Feed.name) }, remoteMediator = FeedRemoteMediator( database = database, postApi = postApi, profileFirestoreService = profileFirestoreService, source = PostSource.Feed, appPreferencesDatastoreService = appPreferencesDatastoreService, configPreferencesDatastoreService = configPreferencesDatastoreService, followersFirestoreService = followersFirestoreService ) ).flow.map { it.map { post -> PostMapper.mapToDomain(post, currentUserId) } } }
FeedRemoteMediator实现
@OptIn(ExperimentalPagingApi::class) class FeedRemoteMediator( private val database: AlyvDatabase, private val postApi: PostApi, private val source: PostSource, private val configPreferencesDatastoreService: ConfigPreferencesDatastoreService, private val appPreferencesDatastoreService: AppPreferencesDatastoreService ) : RemoteMediator<Int, PostDb>() { private val postRemoteKeysDao: PostRemoteKeysDao = database.remoteKeysDao() private val postDao: PostDao = database.postDao() override suspend fun initialize(): InitializeAction { val cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS) return if (System.currentTimeMillis() - (postRemoteKeysDao.getCreationTime(source.name) ?: 0) < cacheTimeout ) { InitializeAction.SKIP_INITIAL_REFRESH } else { InitializeAction.LAUNCH_INITIAL_REFRESH } } override suspend fun load( loadType: LoadType, state: PagingState<Int, PostDb> ): MediatorResult { return withContext(Dispatchers.IO){ val page: Int = when (loadType) { LoadType.REFRESH -> { val remoteKeys = getRemoteKeyClosestToCurrentPosition(state) remoteKeys?.nextKey?.minus(1)?.coerceAtLeast(0) ?: 0 } LoadType.PREPEND -> { val remoteKeys = getRemoteKeyForFirstItem(state) val prevKey = remoteKeys?.prevKey prevKey ?: return@withContext MediatorResult.Success(endOfPaginationReached = remoteKeys != null) } LoadType.APPEND -> { val remoteKeys = getRemoteKeyForLastItem(state) val nextKey = remoteKeys?.nextKey nextKey ?: return@withContext MediatorResult.Success(endOfPaginationReached = remoteKeys != null) } } try { val userId = appPreferencesDatastoreService.getUserId() ?: throw IllegalStateException("User id is missing") val request = GetFeedRequest(userId = userId, page = page) val url = when (source) { PostSource.Feed -> configPreferencesDatastoreService.getFeedUrl() PostSource.Discover -> configPreferencesDatastoreService.getDiscoverFeedUrl() else -> "" } ?: throw IllegalStateException("Feed url is missing") val response = postApi.getFeed(url, request) val posts = response.body() ?: emptyList() val endOfPaginationReached = posts.isEmpty() database.withTransaction { val prevKey = if (page > 1) page - 1 else null val nextKey = if (endOfPaginationReached) null else page + 1 val remoteKeys = posts.map { RemoteKeyDb( postId = it.postId, prevKey = prevKey, currentPage = page, nextKey = nextKey, source = source.name ) } val postsForDb = posts.map { post -> PostMapper.mapToDb( post = post, ) } if (loadType == LoadType.REFRESH) { postRemoteKeysDao.clearRemoteKeys(source.name) postDao.clearAllFromSource(source.name) } postRemoteKeysDao.insertAll(remoteKeys) postDao.saveAll(postsForDb) } return@withContext MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) } catch(exception: Exception){ Log.i("FeedRemoteMediator", "Error: ${exception.message}") exception.printStackTrace() return@withContext MediatorResult.Error(exception) } } } // 补充缺失的RemoteKey查询方法,需添加source过滤 private suspend fun getRemoteKeyClosestToCurrentPosition(state: PagingState<Int, PostDb>): RemoteKeyDb? { return state.anchorPosition?.let { position -> state.closestItemToPosition(position)?.postId?.let { postId -> postRemoteKeysDao.getRemoteKeyForPostId(postId, source.name) } } } private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, PostDb>): RemoteKeyDb? { return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull() ?.postId?.let { postId -> postRemoteKeysDao.getRemoteKeyForPostId(postId, source.name) } } private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, PostDb>): RemoteKeyDb? { return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull() ?.postId?.let { postId -> postRemoteKeysDao.getRemoteKeyForPostId(postId, source.name) } } }
Room Dao方法
@Query("SELECT * FROM post WHERE source = :source ORDER BY creation_date DESC") fun getPostsPagingSource(source: String): PagingSource<Int, PostDb>
已尝试的方案
- 自定义PagingSource:可正常刷新并显示两个列表,但出现LazyVerticalGrid布局异常跳动问题
- 在顶层导航Composable中调用
.collectAsLazyPagingItems()后向下传递:问题可解决,但不想采用这种架构方式
修复方案
1. 修复RemoteKey查询的source过滤逻辑
你的RemoteMediator中缺失getRemoteKeyClosestToCurrentPosition等方法的实现,且这些方法必须添加source过滤,否则会读取到其他标签页的RemoteKey,导致分页逻辑混乱,最终触发不必要的REFRESH或数据清空。
确保RemoteKey的Dao方法也添加source过滤:
@Query("SELECT * FROM remote_keys WHERE postId = :postId AND source = :source") suspend fun getRemoteKeyForPostId(postId: String, source: String): RemoteKeyDb?
2. 每个标签页独立管理LazyPagingItems
在每个标签页对应的ViewModel中初始化Pager的Flow并转换为LazyPagingItems,同时用cachedIn将数据流缓存到ViewModel作用域,避免切换标签页时重复创建Flow和重新加载数据:
class FeedViewModel( private val pagerFactory: PagerFactory, currentUserId: String ) : ViewModel() { val pagingItems = pagerFactory.getFeedPager(currentUserId) .cachedIn(viewModelScope) .collectAsLazyPagingItems() } class DiscoverViewModel( private val pagerFactory: PagerFactory, currentUserId: String ) : ViewModel() { val pagingItems = pagerFactory.getDiscoverPager(currentUserId) // 对应推荐页的Pager方法 .cachedIn(viewModelScope) .collectAsLazyPagingItems() }
在Compose中直接使用ViewModel的pagingItems,这样每个标签页的分页数据是独立缓存的,不会因为切换导致数据源被重置。
3. 优化PagingConfig参数
调整PagingConfig,禁用占位符并优化加载参数,减少空数据显示的概率:
config = PagingConfig( pageSize = FEED_PAGE_SIZE, prefetchDistance = FEED_PAGE_SIZE * 2, initialLoadSize = FEED_PAGE_SIZE * 2, enablePlaceholders = false // 禁用占位符,避免空item占位 )
4. 修复自定义PagingSource的LazyVerticalGrid跳动问题
如果坚持使用自定义PagingSource,可通过以下方式解决布局跳动:
- 确保
getRefreshKey逻辑正确,返回当前列表的锚点位置对应的页码 - 使用
rememberLazyGridState()管理滚动状态,并在数据加载完成后恢复滚动位置:
@Composable fun DiscoverFeedScreen(viewModel: DiscoverViewModel = viewModel()) { val gridState = rememberLazyGridState() val pagingItems = viewModel.pagingItems LaunchedEffect(pagingItems.itemCount) { // 数据加载完成后,若当前在顶部则保持位置,避免跳动 if (pagingItems.itemCount > 0 && gridState.firstVisibleItemIndex == 0) { gridState.scrollToItem(0) } } LazyVerticalGrid( state = gridState, columns = GridCells.Fixed(2), verticalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(8.dp) ) { items(pagingItems.itemCount) { index -> // 渲染列表项 } } }
内容的提问来源于stack exchange,提问作者JackSaf
相关产品推荐
相关产品推荐

