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

Compose Multiplatform中BottomBar切换时LazyVerticalGrid滚动状态丢失

Compose Multiplatform中BottomBar切换时LazyVerticalGrid滚动状态丢失的解决办法

问题背景

开发Compose Multiplatform项目,已实现BottomBar底部导航切换逻辑,验证切换到CharactersScreen时CharactersViewModel始终为同一实例未重建。但切换回CharactersScreen时,页面内容可保留,唯独LazyVerticalGrid的滚动状态每次都会回到顶部,尝试过普通列表替代分页列表、自定义Saver、调整remember用法均无效。

核心原因

rememberLazyGridState()的生命周期与Compose可组合项绑定,切换BottomBar页面时,CharactersScreen会被移出组合树,对应的remember状态被销毁;切换回来时重新创建,导致滚动状态丢失。导航配置的saveState=true和restoreState=true仅针对导航栈状态,不会自动保存可组合内部的LazyGrid状态。

解决方案1:将LazyGridState托管到ViewModel

利用ViewModel的生命周期稳定性(与页面导航无关),将滚动状态存储在ViewModel中,确保切换页面时状态不丢失。

修改CharactersViewModel

class CharactersViewModel(val getRandomCharacter: GetRandomCharacter, repository: Repository) : ViewModel() {

    private val _state = MutableStateFlow(CharactersState())
    val state: StateFlow<CharactersState> = _state

    val characters: Flow<PagingData<CharacterModel>> = repository.getAllCharacters()

    // 托管LazyGridState,用官方Saver持久化状态
    val lazyGridState = rememberSaveable(saver = LazyGridState.Saver) {
        LazyGridState()
    }

    init {
        Napier.i { "Created" }
        viewModelScope.launch {
            val character = withContext(Dispatchers.IO) {
                getRandomCharacter()
            }
            _state.update { it.copy(randomCharacter = character) }
        }
    }
}

修改CharactersScreen

替换原本地创建的lazyGridState,改为从ViewModel获取:

fun CharactersScreen(navigateToCharacterDetail: (Int) -> Unit) {

    val charactersViewModel = koinViewModel<CharactersViewModel>()

    val state = charactersViewModel.state.collectAsState()
    val characters = charactersViewModel.characters.collectAsLazyPagingItems()
    // 从ViewModel获取共享的滚动状态
    val lazyGridState = charactersViewModel.lazyGridState

    LazyVerticalGrid(
        modifier = Modifier.fillMaxSize().padding(start = 16.dp, end = 16.dp),
        columns = GridCells.Fixed(2),
        state = lazyGridState,
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        // 原有列表内容保持不变
        item(span = { GridItemSpan(2) }) {
            Column {
                Text(
                    "Characters",
                    color = Color.White,
                    fontSize = 24.sp,
                    modifier = Modifier.padding(vertical = 8.dp)
                )
                RandomCharacter(character = state.value.randomCharacter)
            }
        }

        when {
            characters.loadState.refresh is LoadState.Loading && characters.itemCount == 0 -> {
                item (span = { GridItemSpan(2) }){
                    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                        CircularProgressIndicator(
                            modifier = Modifier.size(64.dp), color = Color.Red
                        )
                    }
                }
            }
            characters.loadState.refresh is LoadState.NotLoading && characters.itemCount == 0 -> {
                item {
                    Text(text = "Todavía no hay personajes")
                }
            }
            else -> {
                items(characters.itemCount, span = { GridItemSpan(1) }) {
                    characters[it]?.let { characterModel ->
                        ItemList(characterModel) { navigateToCharacterDetail(it) }
                    }
                }

                if (characters.loadState.append is LoadState.Loading) {
                    item {
                        Box(
                            modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center
                        ) {
                            CircularProgressIndicator(
                                modifier = Modifier.size(64.dp), color = Color.White
                            )
                        }
                    }
                }
            }
        }
    }
}

解决方案2:利用导航栈SavedStateHandle保存状态

若不想将UI状态与ViewModel绑定,可通过导航栈条目的SavedStateHandle保存和恢复滚动状态。

修改CharactersScreen

fun CharactersScreen(navigateToCharacterDetail: (Int) -> Unit) {

    val charactersViewModel = koinViewModel<CharactersViewModel>()
    // 获取当前导航栈条目的状态存储对象
    val navBackStackEntry = LocalBackStackEntry.current
    val savedStateHandle = navBackStackEntry.savedStateHandle

    val state = charactersViewModel.state.collectAsState()
    val characters = charactersViewModel.characters.collectAsLazyPagingItems()

    // 从SavedStateHandle恢复或创建滚动状态
    val lazyGridState = rememberSaveable(
        saver = LazyGridState.Saver,
        key = "characters_grid_state",
        init = {
            savedStateHandle.get<LazyGridState>("grid_state") ?: LazyGridState()
        }
    )

    // 监听滚动状态变化,实时保存到SavedStateHandle
    LaunchedEffect(lazyGridState) {
        snapshotFlow { 
            lazyGridState.firstVisibleItemIndex to lazyGridState.firstVisibleItemScrollOffset 
        }
            .distinctUntilChanged()
            .collect {
                savedStateHandle["grid_state"] = lazyGridState
            }
    }

    LazyVerticalGrid(
        modifier = Modifier.fillMaxSize().padding(start = 16.dp, end = 16.dp),
        columns = GridCells.Fixed(2),
        state = lazyGridState,
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        // 原有列表内容保持不变
    }
}

注意事项

  • 确保导航配置中的saveState=true和restoreState=true已正确设置(你的代码中已配置,无需修改)。
  • 方案1更简洁,适合UI状态与ViewModel绑定的场景;方案2贴合导航组件状态管理机制,适合分离UI状态和业务逻辑的场景。

内容的提问来源于stack exchange,提问作者Aris Guimerá

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 16:55:07