Jetpack Compose Navigation 入门教程本教程基于 Android 官方 Compose Navigation Codelab用通俗易懂的方式讲解 Compose Navigation 的核心概念和用法。目录什么是 Navigation先认识几个核心概念添加依赖三步走NavController NavHost 路线TabRow 与导航集成解决两个常见问题在目的地之间传递实参在 Screen 中接收实参用回调代替直接传递 NavController深层链接 Deep Link总结与工程化建议1. 什么是 NavigationNavigation 是 Android 的一个 Jetpack 库专门负责在应用内部做页面跳转。在传统的 View 系统里导航靠Intent、FragmentTransactionCompose 时代就靠Navigation Compose。简单说Navigation 解决这几个问题去哪个页面→ 用route路线标识每个页面怎么去→ 用NavController.navigate(route)谁来显示→ 用NavHost它像一幅地图告诉你当前该显示哪个页面2. 先认识几个核心概念2.1 三个关键角色角色做什么NavController导航的大脑负责管理返回栈、做跳转操作NavHost导航的容器负责显示当前目的地ComposableNavGraph导航的地图列出所有可到达的目的地关系NavController 管理 NavGraphNavGraph 通过 NavHost 展示内容。2.2 路线RouteRoute 就是一串字符串用来唯一标识一个页面。比如overview→ 代表概览页accounts→ 代表账户页single_account/{account_type}→ 代表某个具体账户页带参数每个页面必须有唯一的 Route。2.3 返回栈Back Stack当你从 A 跳到 B再跳到 C返回栈就是A → B → C。按返回键时栈顶 C 出栈显示 B。Compose Navigation 自动帮你维护这个栈。3. 添加依赖在app/build.gradle的dependencies中加入dependencies{implementationandroidx.navigation:navigation-compose:{latest_version}}查看最新版Navigation Compose 版本当前版本最近更新时间稳定版候选版Beta 版Alpha 版2026 年 5 月 19 日2.9.8——2.10.0-alpha054. 三步走NavController NavHost 路线4.1 定义目的地可选但推荐先把每个页面抽象成一个对象通常放在一个单独文件里// RallyDestinations.ktinterfaceRallyDestination{valicon:ImageVectorvalroute:String// 唯一标识这个页面的字符串}objectOverview:RallyDestination{overridevaliconIcons.Filled.PieChartoverridevalrouteoverview}objectAccounts:RallyDestination{overridevaliconIcons.Filled.AccountBalanceoverridevalrouteaccounts}objectBills:RallyDestination{overridevaliconIcons.Filled.CreditCardoverridevalroutebills}4.2 创建 NavController在 Compose 的入口函数里用rememberNavController()创建ComposablefunRallyApp(){valnavControllerrememberNavController()// ...}rememberNavController()确保配置变化旋转屏幕等后 NavController 仍然有效。4.3 创建 NavHost 并注册页面把 NavHost 放在Scaffold的内容区域Scaffold{innerPadding-NavHost(navControllernavController,startDestinationOverview.route,// 启动时显示哪个页面modifierModifier.padding(innerPadding)){composable(routeOverview.route){OverviewScreen()}composable(routeAccounts.route){AccountsScreen()}composable(routeBills.route){BillsScreen()}}}startDestination启动时显示的起始页面composable(route xxx)注册一个可导航的页面lambda 里放这个页面要显示的 Composable4.4 执行跳转在需要跳转的地方调用navController.navigate(Accounts.route)5. TabRow 与导航集成5.1 问题App 顶部有一个 TabRow想让点击 Tab 时跳到对应页面。5.2 基础集成给 TabRow 的onTabSelected回调传入导航逻辑RallyTabRow(allScreensrallyTabRowScreens,onTabSelected{newScreen-navController.navigate(newScreen.route)},currentScreencurrentScreen,)5.3 当前页面高亮TabRow 的样式跟随TabRow 需要知道当前在哪个页面以便高亮正确的 Tab。用currentBackStackEntryAsState()实时获取当前目的地的状态ComposablefunRallyApp(){valnavControllerrememberNavController()// 监听返回栈的变化valcurrentBackStackEntrybynavController.currentBackStackEntryAsState()valcurrentDestinationcurrentBackStackEntry?.destination// 根据 route 找到对应的 RallyDestinationvalcurrentScreenrallyTabRowScreens.find{it.routecurrentDestination?.route}?:OverviewScaffold(topBar{RallyTabRow(allScreensrallyTabRowScreens,onTabSelected{newScreen-navController.navigate(newScreen.route)},currentScreencurrentScreen,)}){innerPadding-NavHost(navControllernavController,startDestinationOverview.route,modifierModifier.padding(innerPadding)){// ...}}}6. 解决两个常见问题6.1 问题一多次点击同一个 Tab 会重复创建页面现象连续点两次 “Accounts” Tab返回栈里会出现两个 Accounts。解决使用launchSingleTop true保证栈顶只有一个该目的地。navController.navigate(newScreen.route){launchSingleToptrue}更优雅的做法是封装成一个扩展函数funNavHostController.navigateSingleTopTo(route:String)this.navigate(route){launchSingleToptrue}6.2 问题二Tab 点击行为像 BottomNavigation按下返回键直接回到首页解决配合popUpTo和saveState / restoreStateimportandroidx.navigation.NavGraph.Companion.findStartDestinationfunNavHostController.navigateSingleTopTo(route:String)this.navigate(route){// 弹出到起始目的地避免栈里堆太多页面popUpTo(thisnavigateSingleTopTo.graph.findStartDestination().id){saveStatetrue// 离开时保存状态}launchSingleToptruerestoreStatetrue// 回来时恢复之前的状态}三个参数的含义参数作用launchSingleTop防止同一页面在栈顶重复创建popUpTo跳转前先弹出到起始页防止栈太深saveState / restoreState保存/恢复页面状态比如滚动位置7. 在目的地之间传递实参7.1 什么是实参Route 可以带参数让同一个页面根据不同参数显示不同内容。例如single_account/Checking显示 Checking 账户详情single_account/Home Savings显示另一个。7.2 定义带实参的目的地在对象里定义实参的名称和类型objectSingleAccount:RallyDestination{overridevaliconIcons.Filled.AttachMoneyoverridevalroutesingle_accountconstvalaccountTypeArgaccount_typevalargumentslistOf(navArgument(accountTypeArg){typeNavType.StringType})}7.3 在 NavHost 注册带实参的目的地NavHost(navControllernavController,startDestinationOverview.route,modifierModifier.padding(innerPadding)){// 普通页面composable(routeOverview.route){OverviewScreen(onClickSeeAllAccounts{navController.navigateSingleTopTo(Accounts.route)},onClickSeeAllBills{navController.navigateSingleTopTo(Bills.route)})}// 带实参的页面route 格式为 route/{参数名}composable(route${SingleAccount.route}/{${SingleAccount.accountTypeArg}},argumentsSingleAccount.arguments){backStackEntry-// 从返回栈中取出实参valaccountTypebackStackEntry.arguments?.getString(SingleAccount.accountTypeArg)SingleAccountScreen(accountTypeaccountType)}}7.4 跳转到带实参的页面navController.navigateSingleTopTo(${SingleAccount.route}/Checking)8. 在 Screen 中接收实参在 composable 的 lambda 参数里通过backStackEntry.arguments获取传入的实参composable(route${SingleAccount.route}/{${SingleAccount.accountTypeArg}},argumentsSingleAccount.arguments){backStackEntry-// 从 arguments 中取出 account_typevalaccountTypebackStackEntry.arguments?.getString(SingleAccount.accountTypeArg)?:UserData.accounts.first().nameSingleAccountScreen(accountTypeaccountType)}然后把accountType传给具体的 ScreenComposablefunSingleAccountScreen(accountType:String?){valaccountremember(accountType){UserData.getAccount(accountType)}// ... 显示账户详情}9. 用回调代替直接传递 NavController9.1 问题如果把navController直接传给 ScreenScreen 就和导航库耦合了难以单独预览和测试。9.2 正确做法传回调在 NavHost 的 lambda 里把导航逻辑封装成回调函数传给 Screencomposable(routeOverview.route){OverviewScreen(onClickSeeAllAccounts{navController.navigateSingleTopTo(Accounts.route)},onClickSeeAllBills{navController.navigateSingleTopTo(Bills.route)})}Screen 只需要声明回调参数ComposablefunOverviewScreen(onClickSeeAllAccounts:()-Unit{},onClickSeeAllBills:()-Unit{},onAccountClick:(String)-Unit{}){// 在按钮的 onClick 里调用回调// 不需要知道导航是怎么实现的}好处Screen 和 NavController 解耦可以单独预览 Screen可以轻松替换导航逻辑比如测试时用假的回调10. 深层链接 Deep Link10.1 什么是 Deep LinkDeep Link 就是用一个 URL 直接打开 App 的某个页面。比如https://myapp.com/accounts/Checking→ 直接打开 Checking 账户详情页。10.2 添加 Deep Link在 composable 里添加deepLinkscomposable(route${SingleAccount.route}/{${SingleAccount.accountTypeArg}},argumentsSingleAccount.arguments,deepLinkslistOf(navDeepLink{uriPatternhttps://myapp.com/account/{account_type}})){backStackEntry-valaccountTypebackStackEntry.arguments?.getString(SingleAccount.accountTypeArg)SingleAccountScreen(accountTypeaccountType)}10.3 在 AndroidManifest 中配置activityandroid:name.RallyActivityandroid:exportedtrueintent-filteractionandroid:nameandroid.intent.action.MAIN/categoryandroid:nameandroid.intent.category.LAUNCHER//intent-filter!-- 添加这个 intent-filter 以支持 deep link --intent-filteractionandroid:nameandroid.intent.action.VIEW/categoryandroid:nameandroid.intent.category.DEFAULT/categoryandroid:nameandroid.intent.category.BROWSABLE/dataandroid:schemehttpsandroid:hostmyapp.comandroid:pathPrefix/account//intent-filter/activity10.4 测试 Deep Link用 adb 模拟点击一个 deep linkadb shell am start-aandroid.intent.action.VIEW\-dhttps://myapp.com/account/Checking11. 总结与工程化建议核心 API 一览API作用rememberNavController()创建 NavControllerNavHost(navController, startDestination) { ... }定义导航图容器composable(route xxx) { ... }注册一个可导航页面navController.navigate(route)执行跳转navController.currentBackStackEntryAsState()监听当前目的地navArgument(name) { type NavType.StringType }定义导航实参navDeepLink { uriPattern ... }定义深层链接工程化建议1. 目录结构ui/ navigation/ RallyDestinations.kt # 目的地定义route、icon screens/ overview/ OverviewScreen.kt accounts/ AccountsScreen.kt SingleAccountScreen.kt bills/ BillsScreen.kt RallyActivity.kt # 组装 NavHost 和 TabRow2. 不要直接传递 NavController 给 Screen用回调代替这样 Screen 更容易测试和预览。3. 封装导航扩展函数把navigate逻辑集中管理funNavHostController.navigateSingleTopTo(route:String)this.navigate(route){popUpTo(graph.findStartDestination().id){saveStatetrue}launchSingleToptruerestoreStatetrue}4. 实参定义放在一起把navArgument的定义放到对应的 Destination 对象里保持代码整洁。5. 类型安全Navigation Compose 支持类型安全的实参传递通过SerializableArg/NavType确保实参类型正确。完整示例RallyApp 最终代码ComposablefunRallyApp(){valnavControllerrememberNavController()valcurrentBackStackEntrybynavController.currentBackStackEntryAsState()valcurrentDestinationcurrentBackStackEntry?.destinationvalcurrentScreenrallyTabRowScreens.find{it.routecurrentDestination?.route}?:Overview RallyTheme{Scaffold(topBar{RallyTabRow(allScreensrallyTabRowScreens,onTabSelected{newScreen-navController.navigateSingleTopTo(newScreen.route)},currentScreencurrentScreen,)}){innerPadding-NavHost(navControllernavController,startDestinationOverview.route,modifierModifier.padding(innerPadding)){composable(routeOverview.route){OverviewScreen(onClickSeeAllAccounts{navController.navigateSingleTopTo(Accounts.route)},onClickSeeAllBills{navController.navigateSingleTopTo(Bills.route)},onAccountClick{accountType-navController.navigateSingleTopTo(${SingleAccount.route}/$accountType)})}composable(routeAccounts.route){AccountsScreen(onAccountClick{accountType-navController.navigateSingleTopTo(${SingleAccount.route}/$accountType)})}composable(routeBills.route){BillsScreen()}composable(route${SingleAccount.route}/{${SingleAccount.accountTypeArg}},argumentsSingleAccount.arguments){backStackEntry-valaccountTypebackStackEntry.arguments?.getString(SingleAccount.accountTypeArg)SingleAccountScreen(accountTypeaccountType)}}}}}文档基于 Google Android 官方 Jetpack Compose Navigation Codelab 编写保留了原 Codelab 的核心内容并增加了通俗解释和工程化建议。