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

Android Kotlin中如何从单个PagingSource获取两类分页列表数据

问题根因

你当前的实现存在3个核心错误,导致分页逻辑无法正常运行:

  • Kotlin中函数return后的代码属于不可达代码,你在load()方法里写了两个LoadResult.Page,第二个永远不会执行,单个PagingSource单次加载只能返回一个结果
  • 初始化的两个Pager都传入了无类型区分的MoviePagingSource,没有指定要拉取正在热映还是热门电影,两个分页流的请求逻辑完全混乱
  • Compose层同时收集两个分页流的写法会造成不必要的资源消耗,哪怕只展示其中一个列表,两个分页请求都会同时触发,浪费流量和内存
正确实现方案

1. 改造通用PagingSource,支持区分列表类型

首先定义枚举标识电影列表类型,让PagingSource根据传入的类型调用对应接口,同时补全Paging3必需的getRefreshKey方法,保证刷新、分页恢复逻辑正常:

// 列表类型枚举
enum class MovieListType {
    NOW_PLAYING, POPULAR
}

class MoviePagingSource(
    private val movieRepository: MovieRepository,
    private val listType: MovieListType
) : PagingSource<Int, Movie>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> {
        return try {
            val currentPage = params.key ?: 1
            // 根据传入的列表类型调用对应接口
            val response = when (listType) {
                MovieListType.NOW_PLAYING -> movieRepository.getNowPlayingMovies(currentPage)
                MovieListType.POPULAR -> movieRepository.getPopularMovies(currentPage)
            }

            val isLastPage = response.page >= response.total_pages
            LoadResult.Page(
                data = response.results,
                prevKey = if (currentPage == 1) null else currentPage - 1,
                nextKey = if (isLastPage) null else currentPage + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, Movie>): Int? {
        // 刷新时返回最近访问页面的相邻页,保证分页位置不丢失
        return state.anchorPosition?.let { anchorPos ->
            state.closestPageToPosition(anchorPos)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPos)?.nextKey?.minus(1)
        }
    }
}

2. 优化ViewModel层,用流切换实现单数据源

不要同时创建两个固定的Pager实例,通过flatMapLatest监听当前选中的列表类型,自动切换对应分页流,切换时会自动取消上一个类型的请求,避免资源浪费:

@HiltViewModel
class MainViewModel @Inject constructor(
    private val movieRepository: MovieRepository,
    private val favMovieRepository: FavMovieRepository
) : ViewModel() {

    // 用StateFlow持有当前选中的列表类型,默认展示正在热映
    private val _currentListType = MutableStateFlow(MovieListType.NOW_PLAYING)
    val currentListType = _currentListType.asStateFlow()

    // 分页数据流,类型切换时自动替换数据源
    val moviePagingFlow: Flow<PagingData<Movie>> = _currentListType
        .flatMapLatest { listType ->
            Pager(
                config = PagingConfig(
                    pageSize = 10,
                    enablePlaceholders = false,
                    initialLoadSize = 10
                )
            ) {
                MoviePagingSource(movieRepository, listType)
            }.flow
        }
        .cachedIn(viewModelScope) // 缓存分页数据,转屏等配置变更时不需要重新加载

    // 对外暴露切换列表的方法
    fun switchListType(type: MovieListType) {
        _currentListType.value = type
    }
}

3. 简化Compose层实现

不需要同时收集两个分页流,只需要收集单个分页流即可,切换类型时Paging组件会自动处理数据更新、状态保留:

@Composable
fun MovieListView(viewModel: MainViewModel) {
    val currentListType by viewModel.currentListType.collectAsStateWithLifecycle()
    val movieItems = viewModel.moviePagingFlow.collectAsLazyPagingItems()

    LazyColumn {
        item {
            Row {
                // 下拉选择控件
                PopularityDropDown(
                    currentSelected = currentListType,
                    onTypeSelected = { selectedType ->
                        viewModel.switchListType(selectedType)
                    }
                )
            }
        }

        items(movieItems) { movie ->
            movie?.let {
                MovieCard(movie = it) {
                    // 你的收藏判断逻辑
                    val isFav = favMovieRepository.isFav(it.id)
                    if (isFav) {
                        // 已收藏状态逻辑
                    } else {
                        Log.d(TAG, "MovieListView: ${it.original_title} with id ${it.id} It is Not favorite ")
                    }
                }
            }
        }

        // 统一处理加载、错误状态
        item {
            when (movieItems.loadState.append) {
                is LoadState.Loading -> CircularProgressIndicator(
                    modifier = Modifier.fillMaxWidth().padding(16.dp)
                )
                is LoadState.Error -> {
                    Button(
                        onClick = { movieItems.retry() },
                        modifier = Modifier.fillMaxWidth().padding(16.dp)
                    ) {
                        Text("加载失败,点击重试")
                    }
                }
                else -> {}
            }
        }
    }
}
额外优化建议
  • 如果需要切换列表时同时保留两个列表的滚动位置和已加载数据,可以在ViewModel中通过cachedIn分别缓存两个类型的PagingData,切换时直接取对应缓存即可,不需要每次重建PagingSource
  • PagingConfig可以根据业务调整prefetchDistance参数,控制列表滑到距离底部多少条时触发下一页预加载,默认值等于pageSize
  • 不要在PagingSource中持有Context、ViewModel等生命周期长于PagingSource的对象,避免内存泄漏

内容的提问来源于stack exchange,提问作者Ashish Gautam

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 06:22:04