Skip to content

02_Navigation.md - Navigation 导航组件

目录

  1. Navigation 概述
  2. Navigation Graph 配置
  3. NavHostFragment
  4. SafeArgs 传递数据
  5. DeepLink 处理
  6. 底部导航集成
  7. 动画配置
  8. 面试考点
  9. 最佳实践与常见错误
  10. 参考资料

什么是 Navigation 组件?

Navigation 是 Android Jetpack 的一部分,用于简化应用内的导航实现。它提供了一套统一的框架来管理 Fragment 事务、Back Stack、Deep Link 等导航相关功能。

核心优势:

  • 可视化编辑导航图(Navigation Graph)
  • 自动处理 Fragment 事务和 Back Stack
  • 类型安全的参数传递(SafeArgs)
  • 简化 Deep Link 实现
  • 内置动画支持
  • 与 BottomNavigationView 无缝集成
┌─────────────────────────────────────────────────────────┐
│                    Navigation 架构                       │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌──────────────┐    ┌──────────────┐    ┌───────────┐ │
│  │   NavGraph   │───▶│  NavHost     │───▶│NavController│ │
│  │  (导航图)     │    │  (宿主)       │    │ (控制器)   │ │
│  └──────────────┘    └──────────────┘    └───────────┘ │
│         │                   │                   │       │
│         ▼                   ▼                   ▼       │
│  ┌──────────────┐    ┌──────────────┐    ┌───────────┐ │
│  │  destinations │    │  Fragment    │    │ navigate()│ │
│  │  (目的地)     │    │  Container   │    │ popBackStack││
│  └──────────────┘    └──────────────┘    └───────────┘ │
│                                                         │
└─────────────────────────────────────────────────────────┘
  1. Navigation Graph(导航图):XML 资源文件,定义所有导航目的地和可能的操作
  2. NavHost(导航宿主):显示导航目的地的容器,通常是 NavHostFragment
  3. NavController(导航控制器):管理应用导航的类,控制导航行为

添加依赖

gradle
// app/build.gradle
dependencies {
    // Navigation Fragment
    implementation "androidx.navigation:navigation-fragment-ktx:2.7.7"
    
    // Navigation UI(与 BottomNavigationView 等集成)
    implementation "androidx.navigation:navigation-ui-ktx:2.7.7"
    
    // SafeArgs(类型安全参数传递)
    // 在 project-level build.gradle 中配置
}

创建 Navigation Graph

res/navigation/ 目录下创建导航图 XML 文件:

xml
<!-- res/navigation/nav_graph.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_graph"
    app:startDestination="@id/homeFragment">

    <!-- 首页 Fragment -->
    <fragment
        android:id="@+id/homeFragment"
        android:name="com.example.app.ui.HomeFragment"
        android:label="@string/home"
        tools:layout="@layout/fragment_home">
        
        <!-- 动作:跳转到详情页 -->
        <action
            android:id="@+id/action_homeFragment_to_detailFragment"
            app:destination="@id/detailFragment"
            app:enterAnim="@anim/nav_enter"
            app:exitAnim="@anim/nav_exit"
            app:popEnterAnim="@anim/nav_pop_enter"
            app:popExitAnim="@anim/nav_pop_exit" />
            
        <!-- 参数定义(配合 SafeArgs) -->
        <argument
            android:name="userId"
            app:argType="string"
            android:defaultValue="0" />
    </fragment>

    <!-- 详情页 Fragment -->
    <fragment
        android:id="@+id/detailFragment"
        android:name="com.example.app.ui.DetailFragment"
        android:label="@string/detail"
        tools:layout="@layout/fragment_detail">
        
        <argument
            android:name="itemId"
            app:argType="long" />
        <argument
            android:name="itemTitle"
            app:argType="string"
            app:nullable="true" />
            
        <action
            android:id="@+id/action_detailFragment_to_homeFragment"
            app:destination="@id/homeFragment"
            app:popUpTo="@id/homeFragment"
            app:popUpToInclusive="false" />
    </fragment>

    <!-- 设置页 Fragment -->
    <fragment
        android:id="@+id/settingsFragment"
        android:name="com.example.app.ui.SettingsFragment"
        android:label="@string/settings"
        tools:layout="@layout/fragment_settings" />

    <!-- Activity 作为目的地 -->
    <activity
        android:id="@+id/detailActivity"
        android:name="com.example.app.ui.DetailActivity"
        android:label="@string/detail_activity" />

    <!-- 嵌套导航图(用于 Bottom Navigation) -->
    <navigation
        android:id="@+id/home_nav_graph"
        app:startDestination="@id/homeListFragment">
        
        <fragment
            android:id="@+id/homeListFragment"
            android:name="com.example.app.ui.HomeListFragment"
            android:label="列表" />
            
        <fragment
            android:id="@+id/homeDetailFragment"
            android:name="com.example.app.ui.HomeDetailFragment"
            android:label="详情" />
    </navigation>

</navigation>
属性说明
android:id导航图的唯一标识
app:startDestination起始目的地 ID
android:nameFragment/Activity 的完整类名
android:label目的地标签(可用于 Toolbar 标题)
tools:layout预览时显示的布局
app:destination动作的目标目的地
app:popUpTo弹出到指定目的地
app:popUpToInclusive是否包含 popUpTo 指定的目的地
app:enterAnim/exitAnim进入/退出动画
app:popEnterAnim/popExitAnim返回时的进入/退出动画

动态创建 Navigation Graph

kotlin
// 在代码中动态构建导航图
val navGraph = NavGraph(NavInflater(context, resources).inflate(R.navigation.nav_graph))

// 设置起始目的地
navGraph.setStartDestination(R.id.loginFragment)

// 设置给 NavController
navController.graph = navGraph

多个 Navigation Graph

对于复杂应用,可以使用多个导航图:

kotlin
// 主导航图
val navGraph = navController.navInflater.inflate(R.navigation.nav_graph_main)

// 嵌套导航图
val homeNavGraph = navController.navInflater.inflate(R.navigation.nav_graph_home)
navGraph.addNavigation(homeNavGraph)

navController.graph = navGraph

基本用法

NavHostFragment 是 Navigation 组件提供的 Fragment 容器,用于显示导航目的地。

xml
<!-- activity_main.xml -->
<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="0dp"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

属性说明

属性说明
android:name必须设置为 androidx.navigation.fragment.NavHostFragment
app:navGraph关联的导航图资源
app:defaultNavHost="true"拦截系统返回键,交由 NavController 处理
android:idFragment 容器 ID

在代码中获取 NavController

kotlin
class MainActivity : AppCompatActivity() {
    
    private lateinit var navController: NavController
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // 方式 1:通过 NavHostFragment 获取
        val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        navController = navHostFragment.navController
        
        // 方式 2:通过 Navigation.findNavController (已废弃)
        // navController = Navigation.findNavController(this, R.id.nav_host_fragment)
        
        // 方式 3:通过 View 获取
        // navController = findNavController(R.id.nav_host_fragment)
    }
    
    // 处理返回键
    override fun onSupportNavigateUp(): Boolean {
        return navController.navigateUp() || super.onSupportNavigateUp()
    }
}

在 Fragment 中获取 NavController

kotlin
class HomeFragment : Fragment() {
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 方式 1:通过 View 扩展
        val navController = findNavController()
        
        // 方式 2:通过 Navigation.findNavController
        val navController = Navigation.findNavController(view)
        
        // 方式 3:通过 Activity 获取
        val navController = (activity as MainActivity).navController
        
        // 导航
        navController.navigate(R.id.action_homeFragment_to_detailFragment)
    }
}

自定义 NavHost

如果需要自定义 NavHost,可以继承 NavHostFragment

kotlin
class CustomNavHostFragment : NavHostFragment() {
    
    override fun createNavController(): NavController {
        val navController = super.createNavController()
        
        // 自定义配置
        navController.addOnDestinationChangedListener { controller, destination, arguments ->
            Log.d("NavHost", "导航到:${destination.label}")
        }
        
        return navController
    }
}

SafeArgs 传递数据

SafeArgs 插件配置

SafeArgs 是 Navigation 的 Gradle 插件,用于生成类型安全的参数传递代码。

Project-level build.gradle:

gradle
buildscript {
    dependencies {
        // 添加 SafeArgs 插件
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.7.7"
    }
}

App-level build.gradle:

gradle
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    // 应用 SafeArgs 插件
    id 'androidx.navigation.safeargs.kotlin'
}

dependencies {
    implementation "androidx.navigation:navigation-fragment-ktx:2.7.7"
}

定义参数

在 Navigation Graph 中定义参数:

xml
<fragment
    android:id="@+id/detailFragment"
    android:name="com.example.app.ui.DetailFragment"
    android:label="@string/detail">
    
    <!-- 基本类型参数 -->
    <argument
        android:name="itemId"
        app:argType="long"
        android:defaultValue="0L" />
    
    <argument
        android:name="itemTitle"
        app:argType="string"
        app:nullable="true"
        android:defaultValue="@null" />
    
    <!-- 布尔类型 -->
    <argument
        android:name="isFavorite"
        app:argType="boolean"
        android:defaultValue="false" />
    
    <!-- 枚举类型 -->
    <argument
        android:name="sortType"
        app:argType="com.example.app.SortType"
        android:defaultValue="DATE" />
    
    <!-- 自定义 Parcelable 类型 -->
    <argument
        android:name="userData"
        app:argType="com.example.app.model.User"
        app:nullable="true" />
    
    <!-- 引用类型 -->
    <argument
        android:name="callback"
        app:argType="com.example.app.Callback"
        app:nullable="true" />
        
</fragment>

生成的类

编译后,SafeArgs 会为每个 Fragment 生成两个类:

  1. DetailFragmentArgs:用于接收参数
  2. DetailFragmentDirections:用于构建导航动作

传递参数

kotlin
// 方式 1:使用 Directions 类(推荐)
val action = HomeFragmentDirections
    .actionHomeFragmentToDetailFragment(
        itemId = 123L,
        itemTitle = "商品标题",
        isFavorite = true
    )
findNavController().navigate(action)

// 方式 2:使用 Bundle
val bundle = bundleOf(
    "itemId" to 123L,
    "itemTitle" to "商品标题",
    "isFavorite" to true
)
findNavController().navigate(R.id.detailFragment, bundle)

// 方式 3:通过 NavOptions
val navOptions = NavOptions.Builder()
    .setLaunchSingleTop(true)
    .setPopUpTo(R.id.homeFragment, false)
    .build()

findNavController().navigate(
    R.id.action_homeFragment_to_detailFragment,
    bundle,
    navOptions
)

接收参数

kotlin
class DetailFragment : Fragment() {
    
    // 方式 1:使用 by navArgs() 委托(推荐)
    private val args: DetailFragmentArgs by navArgs()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 类型安全地访问参数
        val itemId: Long = args.itemId
        val itemTitle: String? = args.itemTitle
        val isFavorite: Boolean = args.isFavorite
        
        textView.text = itemTitle
    }
    
    // 方式 2:手动解析(不推荐)
    /*
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        val itemId = arguments?.getLong("itemId") ?: 0L
        val itemTitle = arguments?.getString("itemTitle")
    }
    */
}

自定义类型转换器

对于复杂类型,可以注册自定义 TypeConverter:

kotlin
// 定义数据类
@Parcelize
data class User(
    val id: Long,
    val name: String
) : Parcelable

// 在 Navigation Graph 中使用
<argument
    android:name="user"
    app:argType="com.example.app.model.User"
    app:nullable="true" />

// 或者使用 TypeConverter
class UserTypeConverter {
    @TypeConverter
    fun fromUser(user: User): String {
        return "${user.id}:${user.name}"
    }
    
    @TypeConverter
    fun toUser(data: String): User {
        val parts = data.split(":")
        return User(parts[0].toLong(), parts[1])
    }
}

DeepLink 允许用户通过 URL 直接跳转到应用的特定页面。Navigation 组件简化了 DeepLink 的实现。

方式 1:在 Navigation Graph 中配置

xml
<fragment
    android:id="@+id/detailFragment"
    android:name="com.example.app.ui.DetailFragment"
    android:label="@string/detail">
    
    <argument
        android:name="itemId"
        app:argType="long" />
    
    <deepLink
        android:autoVerify="true"
        app:uri="example://detail/{itemId}" />
    
    <!-- 多个 DeepLink -->
    <deepLink
        app:uri="https://www.example.com/detail/{itemId}" />
    
    <!-- 带查询参数的 DeepLink -->
    <deepLink
        app:uri="example://product?id={itemId}&amp;source={source}" />
        
</fragment>

方式 2:在代码中配置

kotlin
val navGraph = navController.navInflater.inflate(R.navigation.nav_graph)

val detailDestination = navGraph.findNode(R.id.detailFragment)
detailDestination.addDeepLink(
    navDeepLink {
        uriPattern = "example://detail/{itemId}"
        action = Intent.ACTION_VIEW
    }
)

navController.graph = navGraph
kotlin
class DetailFragment : Fragment() {
    
    private val args: DetailFragmentArgs by navArgs()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // DeepLink 传递的参数会自动填充到 args 中
        val itemId = args.itemId
        
        // 获取 DeepLink 的 URI
        val uri = requireActivity().intent?.data
        Log.d("DeepLink", "URI: $uri")
    }
}

配置 AndroidManifest

xml
<application>
    <activity
        android:name=".ui.MainActivity"
        android:exported="true">
        
        <!-- 主入口 -->
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
        
        <!-- DeepLink 配置 -->
        <intent-filter android:autoVerify="true">
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            
            <data android:scheme="https" android:host="www.example.com" />
            <data android:scheme="example" android:host="detail" />
        </intent-filter>
        
    </activity>
</application>

App Links 是 DeepLink 的增强版,支持自动验证域名所有权:

xml
<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    
    <data android:scheme="https" 
          android:host="www.example.com" 
          android:pathPrefix="/detail" />
</intent-filter>

需要配置 assetlinks.json 文件来验证域名所有权。

bash
# 使用 ADB 测试 DeepLink
adb shell am start -W -a android.intent.action.VIEW \
    -d "example://detail/123" com.example.app

# 测试 HTTPS DeepLink
adb shell am start -W -a android.intent.action.VIEW \
    -d "https://www.example.com/detail/123" com.example.app

底部导航集成

与 BottomNavigationView 集成

xml
<!-- activity_main.xml -->
<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="0dp"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:navGraph="@navigation/mobile_navigation"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@id/nav_view" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/nav_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="?android:attr/windowBackground"
        app:menu="@menu/bottom_nav_menu"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

菜单配置

xml
<!-- res/menu/bottom_nav_menu.xml -->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/navigation_home"
        android:icon="@drawable/ic_home"
        android:title="@string/home" />
    <item
        android:id="@+id/navigation_dashboard"
        android:icon="@drawable/ic_dashboard"
        android:title="@string/dashboard" />
    <item
        android:id="@+id/navigation_notifications"
        android:icon="@drawable/ic_notifications"
        android:title="@string/notifications" />
</menu>

在代码中集成

kotlin
class MainActivity : AppCompatActivity() {
    
    private lateinit var navController: NavController
    private lateinit var appBarConfiguration: AppBarConfiguration
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // 获取 NavController
        val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        navController = navHostFragment.navController
        
        // 配置 BottomNavigationView
        val bottomNavigationView = findViewById<BottomNavigationView>(R.id.nav_view)
        
        // 方式 1:使用 NavigationUI 自动设置(推荐)
        bottomNavigationView.setupWithNavController(navController)
        
        // 方式 2:手动监听(需要自定义逻辑时)
        /*
        bottomNavigationView.setOnItemSelectedListener { item ->
            when (item.itemId) {
                R.id.navigation_home -> {
                    navController.navigate(R.id.homeFragment)
                    true
                }
                R.id.navigation_dashboard -> {
                    navController.navigate(R.id.dashboardFragment)
                    true
                }
                else -> false
            }
        }
        */
        
        // 配置 Toolbar
        appBarConfiguration = AppBarConfiguration(
            setOf(R.id.homeFragment, R.id.dashboardFragment, R.id.notificationsFragment)
        )
        
        setupActionBarWithNavController(navController, appBarConfiguration)
    }
    
    override fun onSupportNavigateUp(): Boolean {
        return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
    }
}

与 NavigationRail 集成

xml
<!-- 适用于平板等大屏设备 -->
<androidx.coordinatorlayout.widget.CoordinatorLayout 
    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.navigation.ui.NavigationRailView
        android:id="@+id/nav_rail"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        app:menu="@menu/bottom_nav_menu"
        app:headerLayout="@layout/nav_rail_header" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:navGraph="@navigation/mobile_navigation"
        app:defaultNavHost="true" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>
kotlin
// 在代码中集成
val navRail = findViewById<NavigationRailView>(R.id.nav_rail)
navRail.setupWithNavController(navController)

处理 Fragment 重建问题

BottomNavigationView 切换时,默认会重建 Fragment。可以通过以下方式避免:

kotlin
// 方式 1:使用 NavigationUI 的默认行为(已优化)
bottomNavigationView.setupWithNavController(navController)

// 方式 2:自定义 ItemReselectedListener
bottomNavigationView.setOnItemReselectedListener { item ->
    // 点击已选中的 item 时的处理
    // 默认不执行导航,避免重建 Fragment
}

// 方式 3:使用 Fragment 的保留实例
class HomeFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 保留 Fragment 实例
        retainInstance = true // 已废弃,使用 ViewModel 替代
    }
}

动画配置

在 Navigation Graph 中配置动画

xml
<action
    android:id="@+id/action_homeFragment_to_detailFragment"
    app:destination="@id/detailFragment"
    app:enterAnim="@anim/nav_enter"
    app:exitAnim="@anim/nav_exit"
    app:popEnterAnim="@anim/nav_pop_enter"
    app:popExitAnim="@anim/nav_pop_exit" />

创建动画资源

xml
<!-- res/anim/nav_enter.xml - 进入动画 -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:fromXDelta="100%"
        android:toXDelta="0%"
        android:duration="300" />
    <alpha
        android:fromAlpha="0.0"
        android:toAlpha="1.0"
        android:duration="300" />
</set>

<!-- res/anim/nav_exit.xml - 退出动画 -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:fromXDelta="0%"
        android:toXDelta="-100%"
        android:duration="300" />
    <alpha
        android:fromAlpha="1.0"
        android:toAlpha="0.0"
        android:duration="300" />
</set>

<!-- res/anim/nav_pop_enter.xml - 返回时进入动画 -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:fromXDelta="-100%"
        android:toXDelta="0%"
        android:duration="300" />
</set>

<!-- res/anim/nav_pop_exit.xml - 返回时退出动画 -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:fromXDelta="0%"
        android:toXDelta="100%"
        android:duration="300" />
</set>

使用 Animator(属性动画)

xml
<!-- res/animator/slide_in_right.xml -->
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300"
    android:interpolator="@android:interpolator/fast_out_slow_in">
    <propertyValuesHolder
        android:propertyName="translationX"
        android:valueFrom="1000"
        android:valueTo="0" />
    <propertyValuesHolder
        android:propertyName="alpha"
        android:valueFrom="0"
        android:valueTo="1" />
</objectAnimator>

在代码中配置动画

kotlin
// 方式 1:通过 NavOptions
val navOptions = NavOptions.Builder()
    .setEnterAnim(R.anim.nav_enter)
    .setExitAnim(R.anim.nav_exit)
    .setPopEnterAnim(R.anim.nav_pop_enter)
    .setPopExitAnim(R.anim.nav_pop_exit)
    .build()

navController.navigate(R.id.detailFragment, null, navOptions)

// 方式 2:通过 FragmentTransaction
supportFragmentManager.beginTransaction()
    .setCustomAnimations(
        R.anim.nav_enter,
        R.anim.nav_exit,
        R.anim.nav_pop_enter,
        R.anim.nav_pop_exit
    )
    .replace(R.id.container, DetailFragment())
    .addToBackStack(null)
    .commit()

使用 Material 动画

kotlin
// 使用 Material 组件的共享元素过渡
val navOptions = NavOptions.Builder()
    .setEnterAnim(R.anim.mtrl_bottom_sheet_slide_in)
    .setExitAnim(R.anim.mtrl_bottom_sheet_slide_out)
    .build()

// 共享元素过渡
val sharedElementTransition = FragmentNavigator.Extras.Builder()
    .addSharedElements(
        imageView to "image_transition"
    )
    .build()

navController.navigate(
    R.id.detailFragment,
    bundle,
    navOptions,
    sharedElementTransition
)

禁用动画

kotlin
// 在特定场景下禁用动画
val navOptions = NavOptions.Builder()
    .setEnterAnim(0)
    .setExitAnim(0)
    .setPopEnterAnim(0)
    .setPopExitAnim(0)
    .build()

// 或者使用全局设置
navController.graph = navGraph
navController.setOnDestinationChangedListener { _, _, _ ->
    // 自定义逻辑
}

面试考点

基础问题

Q1: Navigation 组件的核心组件有哪些?

参考答案: Navigation 组件有三个核心组件:

  1. Navigation Graph:XML 资源文件,定义所有导航目的地和动作
  2. NavHost:导航容器,显示目的地内容,通常是 NavHostFragment
  3. NavController:导航控制器,管理导航行为,执行 navigate() 等操作

三者关系:NavController 操作 NavHost,根据 Navigation Graph 的定义进行导航。

Q2: 如何使用 SafeArgs 传递参数?

参考答案:

kotlin
// 1. 配置插件
// build.gradle 中应用 'androidx.navigation.safeargs.kotlin'

// 2. 在 Navigation Graph 中定义参数
<fragment android:id="@+id/detailFragment">
    <argument android:name="itemId" app:argType="long" />
</fragment>

// 3. 传递参数(使用生成的 Directions 类)
val action = HomeFragmentDirections.actionHomeToDetail(itemId = 123L)
navController.navigate(action)

// 4. 接收参数(使用 navArgs 委托)
class DetailFragment : Fragment() {
    private val args: DetailFragmentArgs by navArgs()
    
    override fun onViewCreated(...) {
        val itemId = args.itemId // 类型安全
    }
}

Q3: NavHostFragment 的作用是什么?

参考答案: NavHostFragment 是 Navigation 组件提供的 Fragment 容器,主要作用:

  1. 显示目的地:作为 Fragment 的容器,显示当前导航目的地
  2. 管理 Back Stack:自动管理 Fragment 的回退栈
  3. 处理返回键:设置 defaultNavHost="true" 可拦截系统返回键
  4. 关联导航图:通过 app:navGraph 属性关联导航配置
xml
<androidx.fragment.app.FragmentContainerView
    android:name="androidx.navigation.fragment.NavHostFragment"
    app:navGraph="@navigation/nav_graph"
    app:defaultNavHost="true" />

参考答案:

xml
<!-- 1. 在 Navigation Graph 中配置 -->
<fragment android:id="@+id/detailFragment">
    <deepLink app:uri="example://detail/{itemId}" />
</fragment>

<!-- 2. 在 AndroidManifest 中配置 Intent Filter -->
<activity android:name=".MainActivity" android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="example" android:host="detail" />
    </intent-filter>
</activity>

<!-- 3. 在 Fragment 中接收参数 -->
private val args: DetailFragmentArgs by navArgs()
val itemId = args.itemId

进阶问题

Q5: Navigation 如何处理 Fragment 的 Back Stack?

参考答案:

Navigation 自动管理 Back Stack:

  1. navigate():将新目的地压入栈
  2. popBackStack():弹出栈顶目的地
  3. popUpTo:弹出到指定目的地
kotlin
// 基本导航
navController.navigate(R.id.detailFragment)

// 带 popUpTo 的导航
navController.navigate(R.id.detailFragment) {
    popUpTo(R.id.homeFragment) // 弹出到 homeFragment
    popUpToInclusive = false   // 不弹出 homeFragment 本身
}

// 直接弹出
navController.popBackStack()

// 弹出到特定目的地
navController.popBackStack(R.id.homeFragment, false)

原理:NavController 内部维护一个 BackStackEntry 列表,每次导航操作都会更新这个列表,并通知 NavHost 显示相应的 Fragment。

Q6: 如何解决 BottomNavigationView 切换时 Fragment 重建的问题?

参考答案:

问题分析:默认情况下,BottomNavigationView 每次切换都会重新创建 Fragment。

解决方案:

方案 1:使用 NavigationUI(推荐)

kotlin
bottomNavigationView.setupWithNavController(navController)
// NavigationUI 内部已优化,会复用 Fragment 实例

方案 2:自定义 Fragment 工厂

kotlin
class ReuseFragmentFactory : FragmentFactory() {
    private val fragments = mutableMapOf<Class<*>, Fragment>()
    
    override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
        return fragments.getOrPut(loadFragmentClass(classLoader, className)) {
            super.instantiate(classLoader, className)
        }
    }
}

supportFragmentManager.fragmentFactory = ReuseFragmentFactory()

方案 3:使用 ViewModel 保存状态

kotlin
class HomeViewModel : ViewModel() {
    // 在 ViewModel 中保存状态,Fragment 重建后恢复
    val data = MutableLiveData<List<Item>>()
}

Q7: Navigation 的动画原理是什么?

参考答案:

Navigation 动画基于 FragmentTransaction 的 setCustomAnimations() 方法:

  1. 动画资源:支持传统的 View 动画(<set><translate>)和属性动画(<objectAnimator>
  2. 四种动画
    • enterAnim:新 Fragment 进入
    • exitAnim:旧 Fragment 退出
    • popEnterAnim:返回时新 Fragment 进入
    • popExitAnim:返回时旧 Fragment 退出
xml
<action
    app:enterAnim="@anim/slide_in_right"
    app:exitAnim="@anim/slide_out_left"
    app:popEnterAnim="@anim/slide_in_left"
    app:popExitAnim="@anim/slide_out_right" />

原理:NavController 在执行导航时,会创建 FragmentTransaction,调用 setCustomAnimations() 设置动画,然后提交事务。

Q8: 如何实现嵌套 Navigation Graph?

参考答案:

嵌套 Navigation Graph 用于复杂导航场景,如 Bottom Navigation 的每个 Tab 有自己的导航栈:

xml
<!-- 主导航图 -->
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
    app:startDestination="@id/home_nav_graph">

    <!-- 嵌套导航图:首页 -->
    <navigation
        android:id="@+id/home_nav_graph"
        app:startDestination="@id/homeListFragment">
        
        <fragment android:id="@+id/homeListFragment" />
        <fragment android:id="@+id/homeDetailFragment">
            <action app:destination="@id/homeListFragment" />
        </fragment>
    </navigation>

    <!-- 嵌套导航图:个人中心 -->
    <navigation
        android:id="@+id/profile_nav_graph"
        app:startDestination="@id/profileFragment">
        
        <fragment android:id="@+id/profileFragment" />
        <fragment android:id="@+id/settingsFragment" />
    </navigation>

</navigation>

使用场景

  • Bottom Navigation 每个 Tab 独立导航栈
  • 侧边导航栏
  • 复杂的层级导航

最佳实践与常见错误

最佳实践

1. 使用 SafeArgs 进行类型安全的参数传递

kotlin
// ✅ 推荐:使用 SafeArgs
val action = HomeFragmentDirections.actionToDetail(itemId = 123L)
navController.navigate(action)

// ❌ 不推荐:手动传递 Bundle
val bundle = bundleOf("itemId" to 123L)
navController.navigate(R.id.detailFragment, bundle)

2. 使用 by navArgs() 委托接收参数

kotlin
// ✅ 推荐
class DetailFragment : Fragment() {
    private val args: DetailFragmentArgs by navArgs()
    
    override fun onViewCreated(...) {
        val itemId = args.itemId
    }
}

// ❌ 不推荐
override fun onViewCreated(...) {
    val itemId = arguments?.getLong("itemId") ?: 0L
}

3. 使用 popUpTo 避免栈无限增长

kotlin
// ✅ 推荐:登录成功后清除登录栈
navController.navigate(R.id.homeFragment) {
    popUpTo(R.id.loginFragment) { inclusive = true }
}

// ❌ 不推荐:栈会无限增长
navController.navigate(R.id.homeFragment)

4. 使用 AppBarConfiguration 配置 Toolbar

kotlin
// ✅ 推荐
val appBarConfiguration = AppBarConfiguration(
    setOf(R.id.home, R.id.dashboard, R.id.notifications)
)
setupActionBarWithNavController(navController, appBarConfiguration)

// 在 Activity 中
override fun onSupportNavigateUp(): Boolean {
    return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}

5. 使用 ViewModel 保存 Fragment 状态

kotlin
// ✅ 推荐:状态保存在 ViewModel 中
class HomeViewModel : ViewModel() {
    val items = MutableLiveData<List<Item>>()
}

class HomeFragment : Fragment() {
    private val viewModel: HomeViewModel by viewModels()
    
    // Fragment 重建后,数据自动恢复
}

常见错误

错误 1:忘记设置 defaultNavHost

xml
<!-- ❌ 错误:返回键无法正确处理 -->
<androidx.fragment.app.FragmentContainerView
    android:name="androidx.navigation.fragment.NavHostFragment"
    app:navGraph="@navigation/nav_graph" />

<!-- ✅ 正确 -->
<androidx.fragment.app.FragmentContainerView
    android:name="androidx.navigation.fragment.NavHostFragment"
    app:navGraph="@navigation/nav_graph"
    app:defaultNavHost="true" />

错误 2:在错误的时机获取 NavController

kotlin
// ❌ 错误:在 onCreate 中获取 viewLifecycleOwner 相关的 NavController
class MyFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val navController = findNavController() // 可能为 null
    }
}

// ✅ 正确:在 onViewCreated 中获取
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    val navController = findNavController()
}

错误 3:滥用 popUpToInclusive

kotlin
// ❌ 错误:意外清除了不该清除的目的地
navController.navigate(R.id.homeFragment) {
    popUpTo(R.id.homeFragment) { inclusive = true } // 连 homeFragment 也弹出了
}

// ✅ 正确
navController.navigate(R.id.homeFragment) {
    popUpTo(R.id.homeFragment) { inclusive = false } // 保留 homeFragment
}
xml
<!-- ❌ 错误:参数类型定义与实际传递不符 -->
<argument android:name="userId" app:argType="long" />
<!-- DeepLink 传递的是字符串 -->

<!-- ✅ 正确:确保类型匹配 -->
<argument android:name="userId" app:argType="string" />
kotlin
// ❌ 错误:每次 DeepLink 都创建新实例
// 没有配置 launchMode

// ✅ 正确:在 AndroidManifest 中配置
<activity
    android:name=".MainActivity"
    android:launchMode="singleTask"
    android:exported="true">
    <!-- DeepLink 配置 -->
</activity>

参考资料

官方文档

源码阅读

相关文章


总结

Navigation 组件是 Android Jetpack 中用于简化导航实现的核心组件,它提供了:

  1. 可视化导航图:通过 XML 定义导航结构
  2. 类型安全参数传递:SafeArgs 插件生成类型安全的代码
  3. 自动 Back Stack 管理:无需手动管理 Fragment 事务
  4. DeepLink 支持:简化 DeepLink 配置和处理
  5. 动画支持:内置导航动画配置
  6. UI 组件集成:与 BottomNavigationView、Toolbar 等无缝集成

掌握 Navigation 组件对于开发结构清晰、用户体验良好的 Android 应用至关重要。在面试中,Navigation 常与 Fragment、ViewModel、LiveData 等组件结合考察。