Jetpack Compose导航异常:Scanner跳转CodeDetails失败
在Jetpack Compose应用中,尝试从History和Scanner两个页面通过嵌套导航图跳转至CodeDetails页面,History页面跳转正常,但Scanner页面跳转时崩溃,抛出以下异常:
java.lang.IllegalArgumentException: No destination with ID -1689679215 is on the NavController's back stack. The current destination is Destination(0x4a873559) route=code_details/{code_id}
相关代码
Scanner导航图
fun NavGraphBuilder.scannerScreenNavGraph( scannerScreenContent: @Composable () -> Unit, codeDetailsScreenContent: @Composable (codeId: UUID) -> Unit ) { navigation( startDestination = Screen.Scanner.route, route = Screen.ScannerMain.route ) { composable(Screen.Scanner.route) { scannerScreenContent() } composable( route = Screen.CodeDetails.route, arguments = listOf( navArgument(KEY_CODE_ID) { type = CodeNavigationType } ) ) { val codeId = it.arguments?.getSerializable(KEY_CODE_ID) as UUID? ?: throw RuntimeException("Args is null") codeDetailsScreenContent(codeId) } } }
History导航图
fun NavGraphBuilder.historyScreenNavGraph( historyScreenContent: @Composable () -> Unit, codeDetailsScreenContent: @Composable (codeId: UUID) -> Unit ) { navigation( startDestination = Screen.History.route, route = Screen.HistoryMain.route ) { composable(Screen.History.route) { historyScreenContent() } composable( route = Screen.CodeDetails.route, arguments = listOf( navArgument(Screen.KEY_CODE_ID) { type = CodeNavigationType } ) ) { val codeId = it.arguments?.getSerializable(Screen.KEY_CODE_ID) as UUID? ?: throw RuntimeException("Args is null") codeDetailsScreenContent(codeId) } } }
AppNavGraph
@Composable fun AppNavGraph( navHostController: NavHostController, scannerScreenContent: @Composable () -> Unit, codeDetailsScreenContent: @Composable (codeId: UUID) -> Unit, historyScreenContent: @Composable () -> Unit, settingsScreenContent: @Composable () -> Unit ) { NavHost( navController = navHostController, startDestination = Screen.ScannerMain.route ) { historyScreenNavGraph( historyScreenContent, codeDetailsScreenContent ) scannerScreenNavGraph( scannerScreenContent, codeDetailsScreenContent ) composable(Screen.Settings.route) { settingsScreenContent() } } }
Screen定义
sealed class Screen( val route: String ) { object ScannerMain : Screen(ROUTE_SCANNER_MAIN) object Scanner : Screen(ROUTE_SCANNER) object CodeDetails : Screen(ROUTE_CODE_DETAILS) { private const val ROUTE_FOR_ARGS = "code_details" fun getRouteWithArgs(codeId: UUID): String { return "$ROUTE_FOR_ARGS/$codeId" } } object HistoryMain : Screen(ROUTE_HISTORY_MAIN) object History : Screen(ROUTE_HISTORY) object Settings : Screen(ROUTE_SETTINGS) companion object { const val KEY_CODE_ID = "code_id" const val ROUTE_SCANNER_MAIN = "main" const val ROUTE_SCANNER = "scanner" const val ROUTE_CODE_DETAILS = "code_details/{$KEY_CODE_ID}" const val ROUTE_HISTORY = "history" const val ROUTE_HISTORY_MAIN = "history_main" const val ROUTE_SETTINGS = "settings" } }
NavigationState
class NavigationState( val navHostController: NavHostController ) { fun navigateTo(route: String) { navHostController.navigate(route) { popUpTo(navHostController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = true } } fun navigateToCodeDetails(codeId: UUID) { navHostController.navigate(Screen.CodeDetails.getRouteWithArgs(codeId)) } }
问题原因与解决方案
原因分析
核心问题是重复定义了同一路由的Destination:在Scanner和History两个嵌套导航图中,都添加了CodeDetails的composable节点。Jetpack Navigation不允许同一导航图树中存在多个route相同的Destination,这会导致NavController无法正确匹配目标路由,尤其是跨嵌套图跳转时,路由解析出现冲突,最终抛出找不到目标ID的异常。
另外,Screen.CodeDetails.getRouteWithArgs生成的路由code_details/{codeId}对应两个不同的Destination,NavController无法确定跳转目标,进而引发崩溃。
解决方案
方案1:将CodeDetails提升为全局Destination
把CodeDetails的composable从两个嵌套导航图中移除,直接放在根导航图(AppNavGraph)中,让所有嵌套图都能跳转至这个全局路由:
修改后的AppNavGraph:
@Composable fun AppNavGraph( navHostController: NavHostController, scannerScreenContent: @Composable () -> Unit, codeDetailsScreenContent: @Composable (codeId: UUID) -> Unit, historyScreenContent: @Composable () -> Unit, settingsScreenContent: @Composable () -> Unit ) { NavHost( navController = navHostController, startDestination = Screen.ScannerMain.route ) { historyScreenNavGraph(historyScreenContent) scannerScreenNavGraph(scannerScreenContent) composable(Screen.Settings.route) { settingsScreenContent() } // 全局定义CodeDetails composable( route = Screen.CodeDetails.route, arguments = listOf( navArgument(Screen.KEY_CODE_ID) { type = CodeNavigationType } ) ) { val codeId = it.arguments?.getSerializable(Screen.KEY_CODE_ID) as UUID? ?: throw RuntimeException("Args is null") codeDetailsScreenContent(codeId) } } }
修改两个嵌套导航图,移除CodeDetails相关定义:
// Scanner导航图修改后 fun NavGraphBuilder.scannerScreenNavGraph( scannerScreenContent: @Composable () -> Unit ) { navigation( startDestination = Screen.Scanner.route, route = Screen.ScannerMain.route ) { composable(Screen.Scanner.route) { scannerScreenContent() } } } // History导航图修改后 fun NavGraphBuilder.historyScreenNavGraph( historyScreenContent: @Composable () -> Unit ) { navigation( startDestination = Screen.History.route, route = Screen.HistoryMain.route ) { composable(Screen.History.route) { historyScreenContent() } } }
方案2:为不同嵌套图定义唯一路由(不推荐)
如果必须将CodeDetails留在各自嵌套图中,需给它们设置不同路由,比如:
修改Screen类:
sealed class Screen( val route: String ) { // 其他定义不变 object ScannerCodeDetails : Screen("scanner_code_details/{$KEY_CODE_ID}") { fun getRouteWithArgs(codeId: UUID): String { return "scanner_code_details/$codeId" } } object HistoryCodeDetails : Screen("history_code_details/{$KEY_CODE_ID}") { fun getRouteWithArgs(codeId: UUID): String { return "history_code_details/$codeId" } } }
然后在各自嵌套图中使用对应路由,并修改NavigationState的跳转逻辑,根据当前页面选择路由。此方案会增加冗余代码,建议优先使用方案1。
额外注意点
- 确保跳转路由与导航图中定义的完全一致,包括参数占位符格式。
- 嵌套导航图的路由是相对路径,全局路由是绝对路径,跳转全局路由无需添加嵌套图前缀。
内容的提问来源于stack exchange,提问作者TOvchinnikova

