LazyColumn跳转至新页面再返回后丢失滚动位置问题求助
各位大佬好,我最近碰到个挺棘手的问题,折腾了好久都没弄明白原因,想过来请教下:
我有一个MyView Composable,结构是Column套着一个LazyColumn,里面包含了几个头部行、内容行,还有几个横向滚动的LazyRow。但是当我用navController.navigate()跳转到下一页,再通过navController.popBackStack()返回时,这个LazyColumn的滚动状态总是会丢失,直接回到顶部。奇怪的是:
- ViewModel并没有被重建
- 填充那些LazyRow的Flow也没有发射新值
- 而且LazyRow的横向滚动位置是正常保留的
- 我在App其他地方用完全一样的导航逻辑,其他LazyColumn都能正常保留滚动位置,唯独这个不行
下面是我简化后的Composable代码:
@Composable fun MyView( viewModel: MyViewModel, onNavigateToNext: (String) -> Unit, ) { val introItems by viewModel.introItems.collectAsStateWithLifecycle(emptyList()) val row1items by viewModel.row1items.collectAsStateWithLifecycle(emptyList()) val row2items by viewModel.row2items.collectAsStateWithLifecycle(emptyList()) val row3items by viewModel.row3items.collectAsStateWithLifecycle(emptyList()) val row4items by viewModel.row4items.collectAsStateWithLifecycle(emptyList()) val listState = rememberLazyListState() Column( modifier = Modifier.fillMaxSize() ) { LazyColumn( state = listState, contentPadding = PaddingValues(bottom = 8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .padding(horizontal = 8.dp) .windowInsetsPadding(WindowInsets.ime) ) { item(key = "header1") { Text("header1", modifier = Modifier.height(80.dp)) } items(introItems, key = {it.itemID}) { listItem -> Text(listItem.name, modifier = Modifier.height(80.dp)) } item(key = "header2") { Text("header2", modifier = Modifier.height(80.dp)) } item(key = "lazyrow1") { LazyRow { items(row1items) { listItem -> Text(listItem.name, modifier = Modifier.height(150.dp)) } } } item(key = "header3") { Text("header3", modifier = Modifier.height(80.dp)) } item(key = "lazyrow2") { LazyRow { items(row2items) { listItem -> Text(listItem.name, modifier = Modifier.height(150.dp)) } } } item(key = "header4") { Text("header4", modifier = Modifier.height(80.dp)) } item(key = "lazyrow3") { LazyRow { items(row3items) { listItem -> Text(listItem.name, modifier = Modifier.height(150.dp)) } } } item(key = "header5") { Text("header5", modifier = Modifier.height(80.dp)) } item(key = "lazyrow4") { LazyRow { items(row4items) { listItem -> Text( listItem.name, modifier = Modifier .height(150.dp) .clickable { onNavigateToNext(listItem.itemID.toString()) } ) } } } } } }
ViewModel里的Flow是这样实现的:
@OptIn(ExperimentalCoroutinesApi::class) val row1Items: StateFlow<List<MyEntity>> = query .flatMapLatest { queryParams -> myDao.getItemsFlow(queryParams.query) } .map { list -> sortedItems(list) } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5.seconds), initialValue = emptyList(), )
大致逻辑是,当query这个数据类被修改时,会调用DAO的查询方法,然后对结果排序后发射到Flow中。
我自己排查了好久,实在找不到问题所在,想请教下各位有没有遇到过类似的情况,或者能给我一些排查方向?
可能的排查方向与解决方案
结合我自己的经验和对Compose状态的理解,给你几个可以尝试的方向:
- 用
rememberSaveable强制保存LazyListState
默认的rememberLazyListState()是用remember来保存状态的,但当Composable从导航栈中被临时移除时,remember的状态可能会被回收。换成rememberSaveable可以把状态持久化到Bundle中,确保返回时能恢复:
val listState = rememberSaveable(saver = LazyListState.Saver) { LazyListState() }
- 检查所有Item的Key是否稳定唯一
虽然你给introItems设置了itemID作为key,但那些LazyRow里的item没有指定key,这可能导致不必要的重组,间接影响父LazyColumn的状态。给LazyRow的item也加上稳定的key试试:
LazyRow { items(row1items, key = { it.itemID }) { listItem -> Text(listItem.name, modifier = Modifier.height(150.dp)) } }
- 避免不必要的Composable重组
如果onNavigateToNext这个lambda每次重组时都是新的实例,可能会触发整个Composable的不必要重组,进而影响滚动状态。可以用remember把它缓存起来:
val onNavigateToNextRemembered = remember(onNavigateToNext) { onNavigateToNext } // 然后在clickable里用这个缓存后的lambda .clickable { onNavigateToNextRemembered(listItem.itemID.toString()) }
检查WindowInsets的影响
你给LazyColumn加了windowInsetsPadding(WindowInsets.ime),导航返回时IME的Insets可能发生变化,导致LazyColumn的尺寸改变,从而重置滚动位置。可以先临时移除这个modifier,看看问题是否消失。如果是这个原因,可以用derivedStateOf来处理Insets的变化,避免直接影响滚动状态。确认Flow的订阅状态
虽然你说Flow没有发射新值,但SharingStarted.WhileSubscribed(5.seconds)会在页面离开5秒后停止订阅,返回时重新订阅。可以尝试把started参数改成SharingStarted.Lazily,看看是否能解决问题(不过这可能会增加资源消耗,只是用来排查)。
希望这些方法能帮你解决问题,要是有新的发现也欢迎回来分享~
内容来源于stack exchange

