Jetpack Compose导航异常:启动自动跳转失效及页面空白
问题修复:Jetpack Compose 导航异常与自动跳转失效
我希望应用启动时自动导航到SearchResults页面,于是在ViewModel的init块里写了修改uiState的逻辑,还在NavHost里加了条件判断触发导航,但现在遇到两个问题:
- 应用启动后无法自动跳转到结果页,必须手动点击搜索按钮
- 点击搜索按钮后页面空白,只显示Search组件和顶部应用栏,点击返回按钮后才恢复正常
相关代码
FlightApp:
@Composable fun FlightApp() { val viewModel: FlightsViewModel = viewModel(factory = FlightsViewModel.Factory) val uiState = viewModel.uiState.collectAsState() val navController: NavHostController = rememberNavController() val backStackEntry by navController.currentBackStackEntryAsState() val currentScreen = FlightScreen.valueOf( backStackEntry?.destination?.route ?: FlightScreen.Home.name ) val coroutineScope = rememberCoroutineScope() Scaffold( topBar = { AppBar( onBackButtonClick = { navController.navigateUp() //viewModel.resetSearchState() }, currentScreen = currentScreen, canNavigateBack = navController.previousBackStackEntry != null ) } ) {innerPadding -> Column(modifier = Modifier.padding(innerPadding)) { Search( query = uiState.value.initialQuery, onQueryChange = { viewModel.onQueryChange(it) }, onSearch = { coroutineScope.launch { viewModel.onSearch() } navController.navigate(FlightScreen.SearchResults.name) }, onSuggestionClick = { coroutineScope.launch { viewModel.getSearchResults(it) } navController.navigate(FlightScreen.SearchResults.name) }, isActive = uiState.value.searchState == SearchState.Searching, airportSuggestions = uiState.value.airportSuggestions ) NavHost( navController = navController, startDestination = FlightScreen.Home.name, modifier = Modifier .fillMaxSize() .padding(8.dp) ) { composable(route = FlightScreen.Home.name) { HomeScreen( favouriteRoutes = uiState.value.favourites, onFavouriteChange = { viewModel.onFavouriteChange(it) } ) } composable(route = FlightScreen.SearchResults.name) { SearchResults( routes = uiState.value.routes, onFavouriteChange = { viewModel.onFavouriteChange(it) } ) } // 问题来源:此处的条件导航写法错误 if (uiState.value.searchState == SearchState.Searched && uiState.value.routes.isNotEmpty()) { navController.navigate(FlightScreen.SearchResults.name) viewModel.onSearch() } } } } }
ViewModel:
class FlightsViewModel( val flightRepository: FlightRepository ): ViewModel() { private val _uiState = MutableStateFlow(FlightsUiState(SearchState.NoSearch, listOf(), "", listOf(), listOf())) val uiState = _uiState.asStateFlow() init { viewModelScope.launch { updateFavourites() } viewModelScope.launch { initialSearch() } } private suspend fun initialSearch() { flightRepository.lastQuery.collect {lastQuery -> _uiState.update {flightsUiState -> flightsUiState.copy( initialQuery = lastQuery ) } if (_uiState.value.initialQuery != "") { onQueryChange(_uiState.value.initialQuery) onSearch() } } } fun onQueryChange(query: String) { viewModelScope.launch { flightRepository.getSuggestions(query).collect { airportSuggestions -> _uiState.update { flightsUiState -> flightsUiState.copy( initialQuery = query, searchState = SearchState.Searching, airportSuggestions = airportSuggestions ) } } } viewModelScope.launch { flightRepository.saveLastQuery(query) } } fun onSearch() { _uiState.update { it.copy( searchState = SearchState.Searched ) } if (_uiState.value.airportSuggestions.size == 1) { viewModelScope.launch { getSearchResults(_uiState.value.airportSuggestions[0]) } } } private suspend fun updateFavourites() { flightRepository.getFavourites().collect {favourites -> _uiState.update { flightsUiState -> flightsUiState.copy( favourites = favourites ) } } } fun onFavouriteChange(route: Route) { viewModelScope.launch { if (route.isFavourite == 1) { flightRepository.onFavouriteChange(route.copy(isFavourite = 0)) } else { flightRepository.onFavouriteChange(route.copy(isFavourite = 1)) } updateFavourites() } } suspend fun getSearchResults(departureAirport: Airport) { flightRepository.getSearchResults(departureAirport).collect {routes -> _uiState.update {flightsUiState -> flightsUiState.copy( searchState = SearchState.Searched, routes = routes ) } } } companion object { val Factory = viewModelFactory { initializer { val application = (this[APPLICATION_KEY] as FlightApplication) FlightsViewModel(application.container.flightRepository) } } } } data class FlightsUiState( val searchState: SearchState = SearchState.NoSearch, val favourites: List<Route>, val initialQuery: String, val airportSuggestions: List<Airport>, val routes: List<Route> ) enum class SearchState { NoSearch, Searching, Searched }
SearchResults:
@Composable fun SearchResults( routes: List<Route>, onFavouriteChange: (Route) -> Unit ) { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { if (routes.isNotEmpty()) { val departureCode = routes[0].departureCode Text( text = stringResource(R.string.flights_from, departureCode), fontWeight = FontWeight.Bold, modifier = Modifier.padding(vertical = 16.dp) ) FlightsColumn(routes = routes) { onFavouriteChange(it) } } else { Text( text = stringResource(R.string.no_results), textAlign = TextAlign.Center ) } } }
问题原因分析
- 自动导航失效:NavHost内部的条件导航代码是错误用法。NavHost的代码块仅用于定义路由页面,在此处写业务逻辑会导致重组时反复执行,且可能因NavHost未完全初始化而导致导航不生效。
- 点击搜索后页面空白:
- 点击搜索时先执行
navigate跳转,再异步调用viewModel.onSearch(),此时SearchResults页面拿到的routes尚未加载完成,因此显示空白 - NavHost内的错误导航逻辑会造成导航栈混乱,进一步加剧页面显示异常
- 点击搜索时先执行
修复方案
1. 删除NavHost内的错误导航代码
直接移除NavHost中的这段代码:
if (uiState.value.searchState == SearchState.Searched && uiState.value.routes.isNotEmpty()) { navController.navigate(FlightScreen.SearchResults.name) viewModel.onSearch() }
2. 使用LaunchedEffect实现安全自动导航
在FlightApp中添加LaunchedEffect,监听uiState变化,满足条件时触发导航(避免重复跳转):
// 放在Scaffold上方或NavHost外部 LaunchedEffect(uiState.value, currentScreen) { val shouldNavigate = uiState.value.searchState == SearchState.Searched && uiState.value.routes.isNotEmpty() && currentScreen != FlightScreen.SearchResults if (shouldNavigate) { navController.navigate(FlightScreen.SearchResults.name) } }
3. 修正搜索按钮的导航逻辑
移除搜索回调中的直接导航,交给LaunchedEffect统一处理,避免时序问题:
onSearch = { coroutineScope.launch { viewModel.onSearch() } // 删除此处的navigate代码 } onSuggestionClick = { coroutineScope.launch { viewModel.getSearchResults(it) } // 删除此处的navigate代码 }
4. 优化ViewModel初始搜索逻辑
确保初始搜索时,建议列表加载完成后再执行搜索,保证状态更新的正确性:
private suspend fun initialSearch() { flightRepository.lastQuery.collect { lastQuery -> if (lastQuery.isNotEmpty()) { _uiState.update { it.copy(initialQuery = lastQuery) } // 等待建议列表加载完成 val suggestionsJob = viewModelScope.launch { flightRepository.getSuggestions(lastQuery).collect { suggestions -> _uiState.update { it.copy( airportSuggestions = suggestions, searchState = SearchState.Searching ) } } } suggestionsJob.join() onSearch() flightRepository.saveLastQuery(lastQuery) } } }
5. 可选:调整页面布局
如果Search组件不需要在所有页面显示,可将其移入HomeScreen,避免导航到SearchResults时仍显示Search组件。
修改后的完整FlightApp代码
@Composable fun FlightApp() { val viewModel: FlightsViewModel = viewModel(factory = FlightsViewModel.Factory) val uiState = viewModel.uiState.collectAsState() val navController: NavHostController = rememberNavController() val backStackEntry by navController.currentBackStackEntryAsState() val currentScreen = FlightScreen.valueOf( backStackEntry?.destination?.route ?: FlightScreen.Home.name ) val coroutineScope = rememberCoroutineScope() // 自动导航逻辑 LaunchedEffect(uiState.value, currentScreen) { val shouldNavigate = uiState.value.searchState == SearchState.Searched && uiState.value.routes.isNotEmpty() && currentScreen != FlightScreen.SearchResults if (shouldNavigate) { navController.navigate(FlightScreen.SearchResults.name) } } Scaffold( topBar = { AppBar( onBackButtonClick = { navController.navigateUp() //viewModel.resetSearchState() }, currentScreen = currentScreen, canNavigateBack = navController.previousBackStackEntry != null ) } ) { innerPadding -> Column(modifier = Modifier.padding(innerPadding)) { Search( query = uiState.value.initialQuery, onQueryChange = { viewModel.onQueryChange(it) }, onSearch = { coroutineScope.launch { viewModel.onSearch() } }, onSuggestionClick = { coroutineScope.launch { viewModel.getSearchResults(it) } }, isActive = uiState.value.searchState == SearchState.Searching, airportSuggestions = uiState.value.airportSuggestions ) NavHost( navController = navController, startDestination = FlightScreen.Home.name, modifier = Modifier .fillMaxSize() .padding(8.dp) ) { composable(route = FlightScreen.Home.name) { HomeScreen( favouriteRoutes = uiState.value.favourites, onFavouriteChange = { viewModel.onFavouriteChange(it) } ) } composable(route = FlightScreen.SearchResults.name) { SearchResults( routes = uiState.value.routes, onFavouriteChange = { viewModel.onFavouriteChange(it) } ) } } } } }
内容的提问来源于stack exchange,提问作者Anas
相关产品推荐
相关产品推荐

