You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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"
    }
}
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 10:37:24