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

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仅在用户滚动到列表底部时才加载后续分页数据?


解答

核心问题分析

  1. RemoteMediator逻辑缺失:当前代码对所有LoadType(REFRESH/PREPEND/APPEND)都无条件执行API请求,没有区分场景。PREPEND类型本不需要处理,却也触发了数据加载。
  2. 分页触发逻辑失控:PagingConfig默认的prefetchDistance是pageSize的一半,若未手动调整,会在列表未滚动到底部时就提前触发加载;再加上RemoteMediator未正确判断加载时机,导致连续调用API。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 08:35:06