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á
相关产品推荐
相关产品推荐

