Navigation 是 Jetpack 中专门管理 Fragment 跳转与返回栈的组件。它通过声明式导航图(NavGraph)统一管理目标页面、跳转动画、参数传递,彻底告别手写 FragmentTransaction 的繁琐与易错。
一、Navigation 设计哲学
1.1 传统 Fragment 导航的痛点
在没有 Navigation 的年代,Android 中实现多页面导航需要:
fun navigateToDetail(articleId: Int) { val fragment = DetailFragment.newInstance(articleId) parentFragmentManager.beginTransaction() .replace(R.id.container, fragment) .addToBackStack("detail_$articleId") .setCustomAnimations( R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right ) .commit() }
|
核心痛点:
- 样板代码泛滥:每个页面跳转都需要手写
FragmentTransaction,动画参数、addToBackStack 容易遗漏
- 类型不安全:参数通过
Bundle 传递,运行时 ClassCastException 频发。调用方需要知道”参数名是 articleId 还是 article_id“
- 返回栈管理复杂:
popBackStack 时开发者需要精确知道栈中有哪些 Fragment,容易造成栈状态异常
- 导航逻辑分散:跳转代码散落在 Activity、Fragment、Adapter 各处,难以梳理完整的导航链路
- DeepLink 难实现:外部跳转进应用需要手写 Intent Filter 和路径解析
- BottomNavigation 集成困难:切换 Tab 时是 replace 还是 show/hide?返回栈如何与 Tab 同步?
Navigation 组件通过声明式的方式解决了所有这些问题。
1.2 Navigation 三大核心概念
| 概念 |
说明 |
| NavGraph |
导航图:声明式定义所有目的地(Destination)和它们之间的导航路径(Action) |
| NavHost |
导航容器:一个空壳 ViewGroup,根据导航指示替换展示不同的 Destination |
| NavController |
导航控制器:执行导航操作(navigate、popBackStack 等)的核心对象 |
┌──────────────────────────────────────────────────────┐ │ Activity │ │ ┌──────────────────────────────────────────────┐ │ │ │ NavHostFragment (NavHost) │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ │ │ │ │ │ │ │ │ 当前显示的 Destination (Fragment) │ │ │ │ │ │ │ │ │ │ │ └──────────────────────────────────────┘ │ │ │ │ │ │ │ │ NavController ← 管理返回栈和跳转 │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ [BottomNavigation / DrawerLayout / Toolbar] │ │ ↑ │ │ └── NavigationUI 自动同步 │ └──────────────────────────────────────────────────────┘
|
二、NavGraph:声明式导航图
2.1 XML 方式定义 NavGraph
在 res/navigation/nav_main.xml 中定义:
<?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_main" app:startDestination="@id/homeFragment">
<fragment android:id="@+id/homeFragment" android:name="com.example.ui.home.HomeFragment" android:label="首页" tools:layout="@layout/fragment_home">
<action android:id="@+id/action_home_to_article_list" app:destination="@id/articleListFragment" app:enterAnim="@anim/slide_in_right" app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" />
<action android:id="@+id/action_home_to_settings" app:destination="@id/settingsFragment" />
<action android:id="@+id/action_home_to_detail" app:destination="@id/detailFragment"> <argument android:name="articleId" app:argType="integer" /> </action>
<deepLink android:id="@+id/deepLinkFromNotification" app:uri="myapp://home" /> </fragment>
<fragment android:id="@+id/articleListFragment" android:name="com.example.ui.list.ArticleListFragment" android:label="文章列表" tools:layout="@layout/fragment_article_list">
<action android:id="@+id/action_list_to_detail" app:destination="@id/detailFragment" /> </fragment>
<fragment android:id="@+id/detailFragment" android:name="com.example.ui.detail.DetailFragment" android:label="详情页" tools:layout="@layout/fragment_detail">
<argument android:name="articleId" app:argType="integer" android:defaultValue="0" />
<argument android:name="articleTitle" app:argType="string" android:defaultValue="" app:nullable="true" /> </fragment>
<fragment android:id="@+id/settingsFragment" android:name="com.example.ui.settings.SettingsFragment" android:label="设置" />
<activity android:id="@+id/loginActivity" android:name="com.example.ui.login.LoginActivity" android:label="登录" />
<dialog android:id="@+id/confirmDialog" android:name="com.example.ui.common.ConfirmDialogFragment" android:label="确认删除"> <argument android:name="message" app:argType="string" /> </dialog>
<navigation android:id="@+id/onboarding_flow" app:startDestination="@id/welcomeFragment">
<fragment android:id="@+id/welcomeFragment" android:name="com.example.onboarding.WelcomeFragment" /> <fragment android:id="@+id/tosFragment" android:name="com.example.onboarding.TosFragment" /> <fragment android:id="@+id/signUpFragment" android:name="com.example.onboarding.SignUpFragment" /> </navigation>
<action android:id="@+id/action_global_login" app:destination="@id/loginActivity" />
</navigation>
|
2.2 程序化 DSL 方式定义 NavGraph(Kotlin)
如果需要动态构建导航图,使用 Kotlin DSL:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main)
val navHostFragment = supportFragmentManager .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navHostFragment.navController.apply { graph = createNavGraph() } }
private fun createNavGraph(): NavGraph { return navController.createGraph(startDestination = "home") {
fragment<HomeFragment>("home") { label = "首页" }
fragment<DetailFragment>("detail/{articleId}") { label = "详情" argument("articleId") { type = NavType.IntType defaultValue = 0 } }
activity<LoginActivity>("login")
navigation("onboarding", startDestination = "welcome") { fragment<WelcomeFragment>("welcome") fragment<TosFragment>("tos") fragment<SignUpFragment>("sign_up") } } } }
|
2.3 嵌套导航图(Nested NavGraph)的实践
嵌套导航图用于将相关页面组织在一起,便于模块化和代码复用:
场景:引导流程(Onboarding)
onboarding_flow: Welcome → ToS → SignUp → Complete ↑___________________________| (完成后弹出整个引导流程)
主流程: Home → List → Detail
|
findNavController().navigate(R.id.onboarding_flow)
findNavController().navigate( AppNavGraphDirections.actionGlobalHome(), NavOptions.Builder() .setPopUpTo(R.id.onboarding_flow, inclusive = true) .build() )
|
三、NavHostFragment:导航容器
3.1 在 Activity 中配置 NavHostFragment
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="0dp" app:defaultNavHost="true" app:navGraph="@navigation/nav_main" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@id/bottom_nav" />
<com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottom_nav" android:layout_width="match_parent" android:layout_height="wrap_content" app:menu="@menu/bottom_nav_menu" app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
|
关键属性解释:
android:name="androidx.navigation.fragment.NavHostFragment":指定容器 Fragment 的实现类
app:navGraph="@navigation/nav_main":绑定导航图
app:defaultNavHost="true":拦截系统返回键,由 NavController 管理返回栈
3.2 NavController 的获取方式
val navController = findNavController(R.id.nav_host_fragment)
val navController = findNavController()
val navController = view.findNavController()
val navController = rememberNavController()
|
四、Safe Args:类型安全的参数传递
4.1 Safe Args 插件配置
plugins { id("androidx.navigation.safeargs.kotlin") version "2.7.7" apply false }
plugins { id("androidx.navigation.safeargs.kotlin") }
|
4.2 生成代码的使用
Safe Args 为每个 Action 生成 *Directions 类,为每个有参数的 Destination 生成 *Args 类。
发送参数:
val action = HomeFragmentDirections .actionHomeToDetail( articleId = 123, articleTitle = "Kotlin 协程深入理解" ) findNavController().navigate(action)
|
接收参数:
class DetailFragment : Fragment() {
private val args: DetailFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val articleId = args.articleId val articleTitle = args.articleTitle } }
|
4.3 支持的参数类型
Safe Args 支持以下类型:
| NavType |
Kotlin/Java 类型 |
示例 |
NavType.IntType |
Integer |
argument android:name="count" app:argType="integer" |
NavType.StringType |
String |
argument android:name="query" app:argType="string" |
NavType.BoolType |
Boolean |
argument android:name="showHeader" app:argType="boolean" |
NavType.FloatType |
Float |
argument android:name="rating" app:argType="float" |
NavType.LongType |
Long |
argument android:name="timestamp" app:argType="long" |
NavType.IntArrayType |
IntArray |
argument android:name="ids" app:argType="integer[]" |
NavType.StringArrayType |
StringArray |
argument android:name="tags" app:argType="string[]" |
NavType.ReferenceType |
@IdRes Int |
资源引用 ID |
自定义 Parcelable |
实现 Parcelable 的类 |
argument android:name="user" app:argType="com.example.User" |
自定义 Serializable |
实现 Serializable 的类 |
argument android:name="config" app:argType="com.example.Config" |
自定义 Enum |
Kotlin enum class |
argument android:name="type" app:argType="com.example.Type" |
五、DeepLink 与外部导航
5.1 隐式 DeepLink(通过 Intent Filter)
在 NavGraph 中声明 deepLink:
<fragment android:id="@+id/articleDetailFragment" android:name="com.example.ArticleDetailFragment"> <argument android:name="articleId" app:argType="integer" />
<deepLink app:uri="https://www.example.com/articles/{articleId}" app:action="android.intent.action.VIEW" />
<deepLink app:uri="myapp://article/{articleId}" /> </fragment>
|
当用户点击 https://www.example.com/articles/42 的链接时,Android 会自动导航到 articleDetailFragment,articleId 参数为 42。
5.2 显式 DeepLink(PendingIntent)
val pendingIntent = NavDeepLinkBuilder(context) .setGraph(R.navigation.nav_main) .setDestination(R.id.articleDetailFragment) .setArguments(Bundle().apply { putInt("articleId", 42) }) .createPendingIntent()
val notification = NotificationCompat.Builder(context, CHANNEL_ID) .setContentTitle("新文章推荐") .setContentText("点击查看详情") .setContentIntent(pendingIntent) .setAutoCancel(true) .build()
|
5.3 手动处理 DeepLink
val deepLinkRequest = NavDeepLinkRequest.Builder .fromUri("https://www.example.com/articles/42".toUri()) .build()
navController.navigate(deepLinkRequest)
|
六、NavigationUI:与 UI 组件集成
6.1 BottomNavigationView 集成
val navController = findNavController(R.id.nav_host_fragment) val bottomNav = binding.bottomNavigationView
bottomNav.setupWithNavController(navController)
|
menu item id 必须与 NavGraph 中 destination id 一致:
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/homeFragment" <!-- 必须与 NavGraph 中 destination id 一致 --> android:icon="@drawable/ic_home" android:title="首页" /> <item android:id="@+id/articleListFragment" android:icon="@drawable/ic_list" android:title="文章" /> <item android:id="@+id/settingsFragment" android:icon="@drawable/ic_settings" android:title="设置" /> </menu>
|
6.2 多返回栈支持(Multiple Back Stacks)
Navigation 2.4+ 支持多返回栈,每个 BottomNavigation tab 有独立的返回栈:
bottomNav.setupWithNavController(navController)
|
当用户:
- 在 Tab A 深入到第三层页面
- 切换到 Tab B
- 再切换回 Tab A
Tab A 的返回栈状态不会有任何损失,之前所在的第三层页面仍然保留。
6.3 DrawerLayout / NavigationView 集成
val drawerLayout = binding.drawerLayout val navView = binding.navigationView
NavigationUI.setupWithNavController(navView, navController)
appBarConfiguration = AppBarConfiguration( setOf(R.id.homeFragment, R.id.settingsFragment), drawerLayout ) NavigationUI.setupWithNavController( toolbar, navController, appBarConfiguration )
|
6.4 AppBarConfiguration
val appBarConfiguration = AppBarConfiguration( topLevelDestinationIds = setOf( R.id.homeFragment, R.id.articleListFragment, R.id.settingsFragment ), fallbackOnNavigateUpListener = ::onSupportNavigateUp )
NavigationUI.setupActionBarWithNavController( this, navController, appBarConfiguration )
|
七、返回栈控制与高级导航
7.1 NavOptions 详解
val navOptions = NavOptions.Builder() .setEnterAnim(R.anim.slide_in_right) .setExitAnim(R.anim.slide_out_left) .setPopEnterAnim(R.anim.slide_in_left) .setPopExitAnim(R.anim.slide_out_right)
.setPopUpTo( destinationId = R.id.homeFragment, inclusive = true, saveState = false )
.setLaunchSingleTop(true)
.setRestoreState(true) .build()
findNavController().navigate( R.id.detailFragment, args, navOptions )
|
7.2 常见导航模式
findNavController().navigate( R.id.homeFragment, null, NavOptions.Builder() .setPopUpTo(R.id.nav_main, inclusive = true) .build() )
findNavController().navigateUp()
findNavController().popBackStack()
findNavController().popBackStack(R.id.homeFragment, inclusive = false)
findNavController().navigate( destinationId, null, NavOptions.Builder() .setLaunchSingleTop(true) .build() )
|
7.3 使用 savedStateHandle 传递返回结果
类似 startActivityForResult,Navigation 2.3+ 支持在返回时将结果传回上一个 Destination:
findNavController().previousBackStackEntry?.savedStateHandle?.apply { set("result_key", "已阅读文章 42") set("is_favorited", true) } findNavController().navigateUp()
findNavController().currentBackStackEntry?.savedStateHandle ?.getLiveData<String>("result_key") ?.observe(viewLifecycleOwner) { result -> Toast.makeText(context, result, Toast.LENGTH_SHORT).show() }
|
八、条件导航与登录流程
8.1 全局登录拦截
这是最常见的条件导航场景:用户想访问需要登录的页面,如果未登录则先跳转到登录页。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
val navController = findNavController(R.id.nav_host_fragment)
navController.addOnDestinationChangedListener { _, destination, _ -> if (destination.id in listOf( R.id.profileFragment, R.id.checkoutFragment, R.id.orderHistoryFragment )) { val isLoggedIn = UserSession.isLoggedIn if (!isLoggedIn) { } } } } }
|
更好的方式是在 ViewModel 中管理导航逻辑:
class ProfileViewModel : ViewModel() { private val _navigationEvent = MutableLiveData<Event<NavDirections>>() val navigationEvent: LiveData<Event<NavDirections>> = _navigationEvent
fun onProfileClicked() { if (UserSession.isLoggedIn) { _navigationEvent.value = Event( HomeFragmentDirections.actionHomeToProfile() ) } else { _navigationEvent.value = Event( HomeFragmentDirections.actionHomeToLogin() ) } } }
class Event<out T>(private val content: T) { private var hasBeenHandled = false fun getContentIfNotHandled(): T? { return if (hasBeenHandled) null else { hasBeenHandled = true content } } }
|
九、导航测试
9.1 TestNavHostController
@RunWith(AndroidJUnit4::class) class NavigationTest {
@Test fun testNavigateToDetail() { val navController = TestNavHostController( ApplicationProvider.getApplicationContext() )
val scenario = launchFragmentInContainer<HomeFragment>( themeResId = R.style.Theme_MyApp )
scenario.onFragment { fragment -> navController.setGraph(R.navigation.nav_main) Navigation.setViewNavController(fragment.requireView(), navController) }
navController.navigate( HomeFragmentDirections.actionHomeToDetail(articleId = 1) )
assertThat(navController.currentDestination?.id) .isEqualTo(R.id.detailFragment) } }
|
十、Navigation 2.8+ 新特性
10.1 类型安全路径(Type-Safe Routes)
Navigation 2.8.0+ 引入了基于 Kotlin 序列化的类型安全导航路由:
@Serializable data class ArticleDetail(val articleId: Int, val articleTitle: String = "")
@Serializable object ArticleList
@Serializable object Settings
navController.navigate(ArticleDetail(articleId = 42, articleTitle = "Hello"))
NavHost(navController = navController, startDestination = ArticleList) { composable<ArticleList> { ArticleListScreen(onItemClick = { articleId -> navController.navigate(ArticleDetail(articleId = articleId)) }) } composable<ArticleDetail> { backStackEntry -> val detail: ArticleDetail = backStackEntry.toRoute() DetailScreen(articleId = detail.articleId) } }
|
这彻底替代了 Safe Args 插件,用 Kotlin 的编译时类型安全保证导航参数的正确性,无需 Gradle 插件。
面试常考问题
Q1:Navigation 与手动 FragmentTransaction 对比优势?
- 类型安全的 Safe Args 避免运行时类型转换错误
- 声明式导航图可视化,一目了然应用的整体导航结构
- 内置动画支持(进入/退出/弹出进入/弹出退出四种动画)
- 自动处理返回栈与 DeepLink
- 与 BottomNavigationView、DrawerLayout、Toolbar 无缝集成
- 支持多返回栈(每个 Tab 独立管理)
savedStateHandle 传递返回结果代替 startActivityForResult
- 减少
FragmentManager 样板代码 80% 以上
Q2:NavController 的作用域与获取方式?
每个 NavHostFragment 有独立的 NavController — — 作用域是它所在的 Fragment/View 树。
Fragment.findNavController():查找当前 Fragment 所属的 NavController。该 Fragment 必须在 NavHostFragment 的后代 View 树内,否则抛出 IllegalStateException
View.findNavController():通过 View 树向上查找
Activity.findNavController(@IdRes viewId):通过指定 NavHostFragment 的 ID 查找
Q3:如何在 ViewModel 级别驱动导航?
推荐方式:
- ViewModel 通过
SingleLiveEvent 或 Channel<NavDirections> 暴露导航事件
- View 层(Fragment/Activity)观察事件并调用
NavController.navigate()
- 不要在 ViewModel 中持有
NavController 引用,这违反 ViewModel 应独立于 UI 框架的原则
- Navigation 2.8+ 的类型安全路由使这一模式更加自然
Q4:Navigation 如何处理配置变更?
Navigation 在配置变更时自动恢复状态:
- Fragment 的
savedInstanceState 由 FragmentManager 管理,Navigation 只是创建和替换 Fragment
NavController 本身的状态(返回栈、当前 destination)由 NavHostFragment 保存
savedStateHandle 的数据在配置变更和进程重建时都存活
- 导航参数通过
Bundle 保存,配置变更时自动恢复
- 使用
navArgs() 委托的参数值在配置变更后依然有效
Q5:Safe Args 生成的代码长什么样?
Safe Args 生成的代码本质上是普通的 Fragment 导航代码。它生成了:
*Directions 类:包含 action*() 方法,内部创建 NavDirections(包含 destinationId、Bundle 参数、NavOptions)
*Args 类:提供 fromBundle(Bundle) 静态方法,委托给 navArgs() 使用的 NavArgsLazy
这使得代码完全类型安全 —— 如果参数类型不匹配,编译就会失败,而非运行时崩溃。