从详情页返回时恢复Jetpack Compose前一个Composable的状态
Jetpack Compose 导航返回后页面状态丢失问题
我在Jetpack Compose中遇到了状态与导航相关的问题:带搜索栏的页面A,点击搜索结果跳转到详情页面,返回页面A时会回到初始状态(搜索栏清空、无搜索结果)。尝试过saveState和restoreState但无效,不确定是导航配置还是其他问题。
提供的代码
NavHost 代码
SharedTransitionLayout { NavHost( modifier = Modifier.fillMaxSize().padding(padding).background(MaterialTheme.colorScheme.background), navController = navController, startDestination = CardSearchRoute ) { composable<CardSearchRoute> { val viewModel = koinViewModel<CardSearchViewModel>() val state by viewModel.state.collectAsStateWithLifecycle() CardSearchScreen( state = state, onEvent = viewModel::onEvent, animatedVisibilityScope = this, onCardClicked = { navController.navigate(CardDetailRoute(cardID = it.id)) } ) } composable<CardDetailRoute> { backStackEntry -> val cardID = backStackEntry.toRoute<CardDetailRoute>().cardID val viewmodel = koinViewModel<CardDetailViewModel>(parameters = { parametersOf(cardID) }) val state by viewmodel.state.collectAsStateWithLifecycle() CardDetailScreen( state = state, animatedVisibilityScope = this, onEvent = {} ) } composable<ComingSoonRoute> { todoScreen("Collection") } composable<ScanRoute> { todoScreen("Scanning") } } }
搜索页面代码
@OptIn(ExperimentalSharedTransitionApi::class, ExperimentalMaterial3Api::class) @Composable fun SharedTransitionScope.CardSearchScreen( state: UIState, onEvent: (UIEvent) -> Unit, onCardClicked: (Card) -> Unit, animatedVisibilityScope: AnimatedVisibilityScope ) { LaunchedEffect(Unit) { onEvent(UIEvent.OnSearchBarTextChanged("")) } Column( modifier = Modifier .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally ) { SearchBar( windowInsets = WindowInsets(top = 0), modifier = Modifier.padding(16.dp).fillMaxWidth(), shape = RoundedCornerShape(corner = CornerSize(8.dp)), inputField = { SearchBarDefaults.InputField( query = state.searchBarText ?: "", onQueryChange = { onEvent(UIEvent.OnSearchBarTextChanged(it)) }, onSearch = { }, expanded = false, onExpandedChange = { }, placeholder = { Text("Search by name") }, colors = TextFieldDefaults.colors( focusedIndicatorColor = Color.White, cursorColor = Color.White, focusedLabelColor = Color.White, unfocusedPlaceholderColor = Color.White.copy(alpha = 0.5f), focusedPlaceholderColor = Color.White.copy(alpha = 0.5f) ), leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, ) }, expanded = false, onExpandedChange = { }, ) { } Box( modifier = Modifier.fillMaxHeight() ) { if (state.isLoading) { CircularProgressIndicator( modifier = Modifier.align(Alignment.Center) ) } else { val cards = state.foundCards.map { it }.toSet() LazyColumn ( horizontalAlignment = Alignment.CenterHorizontally ) { item{ CardCarrousel( title = "LEADERS", cards = cards, type = CardType.LEADER, onCardClicked = onCardClicked, animatedVisibilityScope = animatedVisibilityScope ) } item{ CardCarrousel( title = "BASES", cards = cards, type = CardType.BASE, onCardClicked = onCardClicked, animatedVisibilityScope = animatedVisibilityScope ) } item{ CardCarrousel( title = "UNITS", cards = cards, type = CardType.UNIT, onCardClicked = onCardClicked, animatedVisibilityScope = animatedVisibilityScope ) } item{ CardCarrousel( title = "EVENTS", cards = cards, type = CardType.EVENT, onCardClicked = onCardClicked, animatedVisibilityScope = animatedVisibilityScope ) } item{ CardCarrousel( title = "UPGRADES", cards = cards, type = CardType.UPGRADE, onCardClicked = onCardClicked, animatedVisibilityScope = animatedVisibilityScope ) } } } } } }
ViewModel 代码
class CardSearchViewModel( private val searchCardUseCase: SearchCardUseCase ) : ViewModel() { private val _state = MutableStateFlow(UIState()) val state = _state.asStateFlow() fun onEvent(event: UIEvent) { when (event) { is UIEvent.OnSearchBarTextChanged ->{ _state.update { it.copy( searchBarText = event.text ) } searchJob?.cancel() search() } } } var searchJob: Job? = null private fun search() { searchJob = viewModelScope.launch(Dispatchers.IO) { searchCardUseCase.invoke(_state.value.searchBarText!!).collect { result -> result.onLoading { _, _ -> _state.update { it.copy( isLoading = true, foundCards = emptySet() ) } }.onSuccess { foundCards -> _state.update { it.copy( isLoading = false, foundCards = foundCards.sortedBy { it.name }.toSet() ) } }.onFailure { _state.update { it.copy( isLoading = false ) } } } } } } data class UIState( val searchBarText: String? = null, val isLoading: Boolean = false, val foundCards: Set<Card> = emptySet(), val sortType: SortType = SortType.NAME ) sealed class UIEvent { data class OnSearchBarTextChanged(val text: String) : UIEvent() }
问题根源与修复方案
问题根源
- ViewModel生命周期不匹配:当前使用
koinViewModel()获取ViewModel时,默认会在页面进入后台(被压入导航栈)时销毁,返回时重建,导致状态丢失。 - 强制初始化清空状态:搜索页面中的
LaunchedEffect(Unit)会在每次Composable重组(包括返回时)触发,强制清空搜索栏文本,重置状态。
修复步骤
1. 确保ViewModel在导航栈中保留
修改NavHost中CardSearchRoute的ViewModel获取方式,让ViewModel与当前导航栈条目绑定,确保返回时复用原有实例:
composable<CardSearchRoute> { backStackEntry -> val viewModel = koinViewModel<CardSearchViewModel>(scope = backStackEntry) val state by viewModel.state.collectAsStateWithLifecycle() CardSearchScreen( state = state, onEvent = viewModel::onEvent, animatedVisibilityScope = this, onCardClicked = { navController.navigate(CardDetailRoute(cardID = it.id)) } ) }
2. 移除强制清空状态的代码
删除搜索页面中的LaunchedEffect(Unit)代码块,避免返回时重置搜索状态:
// 移除这段代码 LaunchedEffect(Unit) { onEvent(UIEvent.OnSearchBarTextChanged("")) }
如果需要页面首次加载时初始化为空搜索,将逻辑移至ViewModel的init块:
class CardSearchViewModel( private val searchCardUseCase: SearchCardUseCase ) : ViewModel() { init { onEvent(UIEvent.OnSearchBarTextChanged("")) } // 其余代码不变 }
3. 可选:配置变更时的状态保存
如果需要处理屏幕旋转等配置变更场景,可以在ViewModel中使用SavedStateHandle来持久化关键状态(如搜索栏文本):
class CardSearchViewModel( private val searchCardUseCase: SearchCardUseCase, private val savedStateHandle: SavedStateHandle ) : ViewModel() { private val _state = MutableStateFlow(UIState( searchBarText = savedStateHandle.get<String>("searchBarText") ?: "" )) val state = _state.asStateFlow() fun onEvent(event: UIEvent) { when (event) { is UIEvent.OnSearchBarTextChanged ->{ _state.update { it.copy( searchBarText = event.text ) } savedStateHandle.set("searchBarText", event.text) searchJob?.cancel() search() } } } // 其余代码不变 }
验证效果
完成上述修改后,从详情页返回页面A时,ViewModel会保留之前的状态,搜索栏文本和搜索结果都不会被重置,符合预期行为。
内容的提问来源于stack exchange,提问作者flopez
相关产品推荐
相关产品推荐

