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

Compose中从ViewModel的UiState获取分页数据并解决滚动回顶问题

解决PagingData整合进UiState后列表滚动异常问题

背景说明

之前ViewModel单独维护分页数据,实现方式如下:

private var _data: MutableStateFlow<PagingData<Pokemon>> = MutableStateFlow(PagingData.empty())
val data: StateFlow<PagingData<Pokemon>> = _data.asStateFlow()

对应的Composable中可以正常获取分页数据:

val pagingItems = viewModel.data.collectAsLazyPagingItems()

现在为了统一状态管理,把分页数据整合进了UiState对象:

data class UiState(
    var loading: Boolean = false,
    var error: Boolean = false,
    val dataSource: PagingData<Pokemon> = PagingData.empty(),
)

ViewModel的代码也同步修改为:

private var _state: MutableStateFlow<UiState> = MutableStateFlow(UiState(loading = true))
val state: StateFlow<UiState> = _state.asStateFlow()

遇到的问题

尝试用以下方式在Composable中收集UiState里的dataSource时,发现RemoteMediator加载新数据会导致列表自动滚动到接近顶部的位置:

// 此方式无效,分页加载时列表会滚动到顶部
val state by viewModel.state.collectAsState()
val pagingItems = flowOf(state.dataSource).collectAsLazyPagingItems()

对应的列表Composable实现代码如下:

@Composable
fun PokemonListScreen(navController: NavController, pokemon: LazyPagingItems<Pokemon>) {

    LazyColumn(
        modifier = Modifier.padding(4.dp)
    ) {
        item { Spacer(modifier = Modifier.padding(4.dp)) }

        items(count = pokemon.itemCount, key = pokemon.itemKey { it.id }) { index ->
            pokemon[index]?.let { poke ->
                PokemonItem(
                    poke,
                    onRepoClicked = {
                        navController.navigate(Routes.RepoDetailScreen.route)
                    }
                )
            }
        }


        pokemon.apply {
            when {
                loadState.refresh is LoadState.Loading -> {
                    item { PageLoader(modifier = Modifier.fillParentMaxSize()) }
                }

                loadState.refresh is LoadState.Error -> {
                    val error = pokemon.loadState.refresh as LoadState.Error
                    item {
                        ErrorMessage(
                            modifier = Modifier.fillParentMaxSize(),
                            message = error.error.localizedMessage,
                            onClickRetry = { retry() })
                    }
                }

                loadState.refresh is LoadState.NotLoading && pokemon.itemCount > 0 -> {
                    item {
                        ErrorMessage(
                            modifier = Modifier.fillParentMaxSize(),
                            message = stringResource(id = R.string.str_no_results),
                            onClickRetry = { retry() })
                    }
                }

                loadState.append is LoadState.Loading -> {
                    item { LoadingNextPageItem(modifier = Modifier) }
                }

                loadState.append is LoadState.Error -> {
                    val error = pokemon.loadState.append as LoadState.Error
                    item {
                        ErrorMessage(
                            modifier = Modifier,
                            message = error.error.localizedMessage,
                            onClickRetry = { retry() })
                    }
                }
            }
        }
        item { Spacer(modifier = Modifier.padding(4.dp)) }
    }

}

解决方案

问题根源在于flowOf(state.dataSource)每次UiState更新时都会创建一个新的Flow实例,导致collectAsLazyPagingItems()重新订阅,进而触发LazyPagingItems重建,最终引发列表滚动异常。以下两种方法可以解决这个问题:

方法1:在ViewModel中单独暴露dataSource的Flow

修改ViewModel,从state Flow中映射出独立的dataSource Flow,这样Composable可以直接订阅这个Flow:

val dataSourceFlow: StateFlow<PagingData<Pokemon>> = _state.map { it.dataSource }.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5000),
    initialValue = PagingData.empty()
)

然后在Composable中使用:

val pagingItems = viewModel.dataSourceFlow.collectAsLazyPagingItems()

这种方式既保留了UiState的统一管理,又能保证分页数据订阅的稳定性,不会触发不必要的重组。

方法2:在Composable中直接映射原StateFlow(无需修改ViewModel)

如果不想改动ViewModel代码,可以在Composable中直接基于原state Flow进行映射,避免创建新Flow:

val pagingItems = viewModel.state
    .map { it.dataSource }
    .collectAsLazyPagingItems()

这种方式会直接订阅原StateFlow的映射结果,确保每次数据更新时都是同一个Flow实例,不会导致LazyPagingItems频繁重建,自然也不会出现滚动异常。

额外提示

其实LazyPagingItems本身的loadState已经包含了加载状态和错误状态,不一定需要在UiState中重复维护loading和error字段,这样可以减少状态冗余。如果确实需要在UiState中统一管理,注意更新UiState时不要重复发送相同的dataSource实例,避免Flow不必要的发射。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 18:45:54