Jetpack Navigation双入口跳转同一公共页面时分类页崩溃问题
问题场景
现有三个页面:CategoryScreen、CompanyScreen、ProductsScreen,需要实现从CategoryScreen和CompanyScreen都能跳转到公共页ProductsScreen的导航逻辑。实际运行时从CompanyScreen发起跳转完全正常,从CategoryScreen发起跳转时应用直接崩溃,崩溃截图如下:
问题相关实现代码
NavHost 实现
NavHost( navController = navController, startDestination = startDestination, modifier = modifier, ) { homeGraph( windowSizeClass = windowSizeClass, navigateToSubCategories = { navController.navigate(route = "${SubCategoryDestination.destination}/$this") }, navigateToAllCategories = { navController.navigate(route = AllCategoriesDestination.destination) }, nestedGraphs = { allCategoriesGraph(windowSizeClass = windowSizeClass, onBackClick = { navController.popBackStack() }, navigateToProducts = { navController.navigate(route = "${AllProductsDestination.destination}/$this/${ProductsType.CATEGORY.type}") }, nestedGraphs = { allProductsGraph(windowSizeClass = windowSizeClass, onBackClick = {}, navigateToProductDetail = { navController.navigate(ProductDetailDestination.route) }, nestedGraphs = { productDetailGraph() }) }) } ) allCompaniesGraph( windowSizeClass = windowSizeClass, navigateToProducts = { navController.navigate( route = "${AllProductsDestination.destination}/$this/${ProductsType.COMPANY.type}") }, nestedGraphs = { allProductsGraph(windowSizeClass = windowSizeClass, onBackClick = {}, navigateToProductDetail = { navController.navigate(ProductDetailDestination.route) }, nestedGraphs = { productDetailGraph() }) } ) }
CompanyNavigation 文件代码
object AllCompaniesDestination : IFishoppingNavigationDestination { override val route: String get() = "all_companies_route" override val destination: String get() = "all_companies_destination" } @ExperimentalFoundationApi @ExperimentalAnimationApi @ExperimentalMaterial3Api fun NavGraphBuilder.allCompaniesGraph( windowSizeClass: WindowSizeClass, navigateToProducts: String.() -> Unit, nestedGraphs: NavGraphBuilder.() -> Unit, ) { navigation(route = AllCompaniesDestination.route, startDestination = AllCompaniesDestination.destination) { composable(route = AllCompaniesDestination.destination) { CompaniesScreen(windowSizeClass = windowSizeClass, navigateToProducts = navigateToProducts) } nestedGraphs() } }
CategoryNavigation 文件代码
object AllCategoriesDestination : IFishoppingNavigationDestination { override val route: String get() = "all_categories_route" override val destination: String get() = "all_categories_destination" } @ExperimentalAnimationApi @ExperimentalLayoutApi @ExperimentalMaterial3Api fun NavGraphBuilder.allCategoriesGraph( windowSizeClass: WindowSizeClass, onBackClick: () -> Unit, navigateToProducts: String.() -> Unit, nestedGraphs: NavGraphBuilder.() -> Unit, ) { navigation( route = AllCategoriesDestination.route, startDestination = AllCategoriesDestination.destination ) { composable(route = AllCategoriesDestination.destination) { AllCategoriesScreen(windowSizeClass = windowSizeClass, onBackClick = {}, navigateToProducts = { navigateToProducts() }) } nestedGraphs() } }
公共目标页 ProductNavigation 文件代码
object AllProductsDestination : IFishoppingNavigationDestination { override val route: String get() = "all_products_route" override val destination: String get() = "all_products_destination" const val allProductsArg = "allProductsParam" const val allProductsRequestTypeArg = "allProductsRequestTypeParam" } @ExperimentalLayoutApi @ExperimentalMaterial3Api fun NavGraphBuilder.allProductsGraph( windowSizeClass: WindowSizeClass, onBackClick: () -> Unit, navigateToProductDetail: String.() -> Unit, nestedGraphs: NavGraphBuilder.() -> Unit, ) { navigation(route = AllProductsDestination.route, startDestination = AllProductsDestination.destination) { composable(route = AllProductsDestination.destination .plus("/{${AllProductsDestination.allProductsArg}}") .plus("/{${AllProductsDestination.allProductsRequestTypeArg}}"), arguments = listOf( navArgument(AllProductsDestination.allProductsArg) { type = NavType.StringType }, navArgument(AllProductsDestination.allProductsRequestTypeArg) { type = NavType.StringType } )) { ProductsScreen(windowSizeClass = windowSizeClass, onBackClick = onBackClick) } nestedGraphs() } }
崩溃根因
- 公共导航图重复注册:你把
allProductsGraph分别嵌套在allCategoriesGraph和allCompaniesGraph两个独立的业务子导航图内部,相当于同一个导航目标被重复定义在两个不同的父导航域下,导航控制器匹配路由时会出现规则冲突。 - 分类页跳转入口错误:跳
AllCategories页面时你用的路由是AllCategoriesDestination.destination,但allCategoriesGraph的父导航入口路由是AllCategoriesDestination.route,直接跳destination相当于绕过嵌套导航的入口直接访问内部页面,导航控制器无法匹配到注册在这个子图内部的products路由规则,直接抛出找不到目的地的崩溃。 - 路由拼接不统一:跳转Products页面时你用
AllProductsDestination.destination拼接参数,但注册Products路由时的父导航入口是AllProductsDestination.route,路径前缀不匹配也会导致路由找不到。 - 附带的小bug:
AllCategoriesScreen里的onBackClick被硬编码为空函数,传入的返回回调被覆盖,后续返回功能也会异常。
修复方案
- 公共页面的导航图不要嵌套在任何业务子导航图里,把
allProductsGraph移到NavHost的顶级注册位置,和homeGraph、allCompaniesGraph平级。 - 所有跳转嵌套导航图的逻辑,统一走导航图的
route入口,不要直接跳内部的destination。 - 统一Products页面的路由常量,避免手动拼接路径出现前缀不匹配的问题。
- 修复参数透传逻辑,不要覆盖传入的回调函数。
修复后核心代码
修正NavHost注册逻辑
NavHost( navController = navController, startDestination = startDestination, modifier = modifier, ) { homeGraph( windowSizeClass = windowSizeClass, navigateToSubCategories = { navController.navigate(route = "${SubCategoryDestination.destination}/$this") }, // 走分类导航图的route入口 navigateToAllCategories = { navController.navigate(route = AllCategoriesDestination.route) }, nestedGraphs = { allCategoriesGraph(windowSizeClass = windowSizeClass, onBackClick = { navController.popBackStack() }, navigateToProducts = { categoryId -> // 跳公共products页用统一的完整路由 navController.navigate(route = "${AllProductsDestination.fullRoute}/$categoryId/${ProductsType.CATEGORY.type}") } ) } ) allCompaniesGraph( windowSizeClass = windowSizeClass, navigateToProducts = { companyId -> // 跳公共products页用统一的完整路由 navController.navigate( route = "${AllProductsDestination.fullRoute}/$companyId/${ProductsType.COMPANY.type}") } ) // 公共products图作为顶级导航注册,不嵌套在业务子图内 allProductsGraph(windowSizeClass = windowSizeClass, onBackClick = { navController.popBackStack() }, navigateToProductDetail = { navController.navigate(ProductDetailDestination.route) }, nestedGraphs = { productDetailGraph() } ) }
修正ProductNavigation路由配置
object AllProductsDestination : IFishoppingNavigationDestination { override val route: String get() = "all_products_route" override val destination: String get() = "all_products_destination" const val allProductsArg = "allProductsParam" const val allProductsRequestTypeArg = "allProductsRequestTypeParam" // 统一定义完整路由,避免拼接错误 const val fullRoute = "$route/{$allProductsArg}/{$allProductsRequestTypeArg}" } @ExperimentalLayoutApi @ExperimentalMaterial3Api fun NavGraphBuilder.allProductsGraph( windowSizeClass: WindowSizeClass, onBackClick: () -> Unit, navigateToProductDetail: String.() -> Unit, nestedGraphs: NavGraphBuilder.() -> Unit, ) { navigation( route = AllProductsDestination.route, startDestination = AllProductsDestination.fullRoute ) { composable( route = AllProductsDestination.fullRoute, arguments = listOf( navArgument(AllProductsDestination.allProductsArg) { type = NavType.StringType }, navArgument(AllProductsDestination.allProductsRequestTypeArg) { type = NavType.StringType } ) ) { backStackEntry -> // 从栈中取出导航参数传给页面 val targetId = backStackEntry.arguments?.getString(AllProductsDestination.allProductsArg) ?: "" val requestType = backStackEntry.arguments?.getString(AllProductsDestination.allProductsRequestTypeArg) ?: "" ProductsScreen( windowSizeClass = windowSizeClass, onBackClick = onBackClick, targetId = targetId, requestType = requestType ) } nestedGraphs() } }
修正CategoryNavigation的回调透传
@ExperimentalAnimationApi @ExperimentalLayoutApi @ExperimentalMaterial3Api fun NavGraphBuilder.allCategoriesGraph( windowSizeClass: WindowSizeClass, onBackClick: () -> Unit, navigateToProducts: String.() -> Unit, nestedGraphs: NavGraphBuilder.() -> Unit = {}, ) { navigation( route = AllCategoriesDestination.route, startDestination = AllCategoriesDestination.destination ) { composable(route = AllCategoriesDestination.destination) { AllCategoriesScreen( windowSizeClass = windowSizeClass, onBackClick = onBackClick, // 不要硬编码空函数,透传传入的返回回调 navigateToProducts = { categoryId -> navigateToProducts(categoryId) } ) } nestedGraphs() } }
内容的提问来源于stack exchange,提问作者Saeed Noshadi
相关产品推荐
相关产品推荐

