AndroidX RemoteMediator连续调用API:如何实现滚动到底部加载?
问题
正在开发一款研究Jetpack Compose和大型列表的Android兴趣应用,使用漫威Comics REST API(含3万+漫画),API通过limit(最多100条)和offset实现分页。
尝试结合Room与分页库自定义RemoteMediator,但未达到预期:期望LazyList加载完第一批数据后,仅在用户滚动到列表底部时触发下一页API调用,实际却是API被连续调用。
Compose组件代码
val viewModel: ComicViewModel = hiltViewModel() val comics = viewModel.comics.collectAsLazyPagingItems() Scaffold( topBar = { ComicsTopAppBar(onNavigateBack = onNavigateBack) } ) { innerPadding -> Box( modifier = Modifier .fillMaxSize() .padding(innerPadding) ) { if (comics.loadState.refresh is LoadState.Loading) { CircularProgressIndicator( modifier = Modifier.align(Alignment.Center) ) } else { LazyColumn( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { items( count = comics.itemCount, key = comics.itemKey { it.id }, contentType = comics.itemContentType { "MyPagingItems" } ) { index -> val item = comics[index] ComicItem(comic = item!!) } item { if (comics.loadState.append is LoadState.Loading) { CircularProgressIndicator() } } } } } }
自定义RemoteMediator代码
import android.util.Log import androidx.paging.ExperimentalPagingApi import androidx.paging.LoadType import androidx.paging.PagingState import androidx.paging.RemoteMediator import org.research.development.marvel.common.model.database.ComicDO import org.research.development.marvel.viewmodel.ComicRepository private const val TAG = "ComicRemoteMediator" @OptIn(ExperimentalPagingApi::class) class ComicRemoteMediator(private val repository: ComicRepository) : RemoteMediator<Int, ComicDO>() { override suspend fun load(loadType: LoadType, state: PagingState<Int, ComicDO>): MediatorResult { Log.d(TAG, "load() called with: loadType = $loadType") return try { when (loadType) { LoadType.REFRESH -> {} LoadType.PREPEND -> {} LoadType.APPEND -> {} } val endOfPaginationReached = repository.comics(state.config.pageSize) MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) } catch (e: Exception) { MediatorResult.Error(e) } } }
Repository中的comics方法代码
class ComicRepository @Inject constructor(private val service: MarvelComicsApi, val database: MarvelComicDatabase) { private val comicsDO = ArrayList<ComicDO>() suspend fun comics(pageSize: Int): Boolean { var endOfPaginationReached = false comicsDO.clear() yield() service.comics(queryParameters = manufactureQueryParameters(pageSize)) .onLeft { callError -> } .onRight { comicResponse -> comicResponse.data.results.forEach { result -> comicsDO.add( ComicDO( id = result.id, title = result.title, description = result.description ?: "MISSING", onSaleDate = result.dates.first { saleDate -> true }, imageUrl = "${result.thumbnail.path}.${result.thumbnail.extension}" ) ) } database.comicDAO.cache(comicsDO) endOfPaginationReached = comicResponse.data.total == database.comicDAO.count() } comicsDO.clear() return endOfPaginationReached } private suspend fun manufactureQueryParameters(limit: Int, timestamp: String = System.currentTimeMillis().toString()): Map<String, String> = mapOf( "format" to "comic", "formatType" to "comic", "noVariants" to "true", "limit" to limit.toString(), "offset" to database.comicDAO.count().toString(), "ts" to timestamp, "hash" to messageDigest5(generateApiHash(timestamp = timestamp)).toHex(), "apikey" to "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" ) }
当前疑问:是否是因为未加载完所有漫画前始终设置MediatorResult.Success(endOfPaginationReached = false)导致的问题?能否让RemoteMediator仅在用户滚动到列表底部时才加载后续分页数据?
解答
核心问题分析
- RemoteMediator逻辑缺失:当前代码对所有
LoadType(REFRESH/PREPEND/APPEND)都无条件执行API请求,没有区分场景。PREPEND类型本不需要处理,却也触发了数据加载。 - 分页触发逻辑失控:
PagingConfig默认的prefetchDistance是pageSize的一半,若未手动调整,会在列表未滚动到底部时就提前触发加载;再加上RemoteMediator未正确判断加载时机,导致连续调用API。 - endOfPaginationReached判断依赖不稳定:依赖数据库计数和API返回总数的对比,但如果
RemoteMediator被频繁触发,数据库计数会不断更新,导致始终返回false,让分页库认为还有数据需要加载。
修复步骤
1. 修正RemoteMediator的LoadType处理逻辑
仅在REFRESH(首次/刷新加载)和APPEND(滚动到底部)时触发API请求,PREPEND直接返回已到分页末尾:
override suspend fun load(loadType: LoadType, state: PagingState<Int, ComicDO>): MediatorResult { Log.d(TAG, "load() called with: loadType = $loadType") return try { val endOfPaginationReached = when (loadType) { LoadType.REFRESH -> { // 刷新时清空旧数据,加载第一页 database.comicDAO.clearAll() repository.comics(state.config.pageSize) } LoadType.PREPEND -> { // 无需加载前面的数据,直接标记为已到末尾 true } LoadType.APPEND -> { // 先判断当前是否还有未加载的数据 val currentCount = database.comicDAO.count() val totalCount = repository.getTotalComicCount() if (currentCount >= totalCount) { true } else { repository.comics(state.config.pageSize) } } } MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) } catch (e: Exception) { MediatorResult.Error(e) } }
2. 优化Repository的分页计数逻辑
缓存API返回的总漫画数,避免重复计算,同时修正偏移量的准确性:
class ComicRepository @Inject constructor(private val service: MarvelComicsApi, val database: MarvelComicDatabase) { private var totalComicCount = 0 private val comicsDO = ArrayList<ComicDO>() suspend fun comics(pageSize: Int): Boolean { var endOfPaginationReached = false comicsDO.clear() service.comics(queryParameters = manufactureQueryParameters(pageSize)) .onLeft { callError -> } .onRight { comicResponse -> totalComicCount = comicResponse.data.total comicResponse.data.results.forEach { result -> comicsDO.add( ComicDO( id = result.id, title = result.title, description = result.description ?: "MISSING", onSaleDate = result.dates.firstOrNull { /* 补充你的日期筛选逻辑 */ }?.date ?: "", imageUrl = "${result.thumbnail.path}.${result.thumbnail.extension}" ) ) } database.comicDAO.cache(comicsDO) endOfPaginationReached = database.comicDAO.count() >= totalComicCount } comicsDO.clear() return endOfPaginationReached } fun getTotalComicCount() = totalComicCount // 保持原有manufactureQueryParameters方法不变 }
3. 调整PagingConfig的预加载距离
在ViewModel中创建Pager时,设置合理的prefetchDistance,确保仅在接近列表底部时才触发加载:
// ViewModel中的Pager配置示例 val comics = Pager( config = PagingConfig( pageSize = 100, // 和API的limit保持一致 prefetchDistance = 10, // 滚动到距离底部10条时触发加载 enablePlaceholders = false ), remoteMediator = ComicRemoteMediator(repository), pagingSourceFactory = { database.comicDAO.getAllComics() } ).flow.cachedIn(viewModelScope)
关键说明
RemoteMediator的APPEND触发时机完全由PagingConfig.prefetchDistance控制,设置较小的值可以避免提前加载。- 必须严格区分不同
LoadType的处理逻辑,禁止无差别触发API请求。 - 缓存API返回的总数据量,能更准确判断是否到达分页末尾,避免无效请求。
内容的提问来源于stack exchange,提问作者Hector
相关产品推荐
相关产品推荐

