NavController — Jetpack Compose 中的导航控制器实质、方法与管理

作者: IT Sectr 发布日期: 2026-06-29 阅读时间: 7 分钟

NavController 是 Navigation Compose 库的核心组件,管理 Android 应用程序中的导航堆栈和 back stack 状态。通过 NavController 执行屏幕之间的切换、返回上一页面以及路由之间的数据传递。根据 Android Developers (2025),NavController 是任何具有多个屏幕的 Compose 应用程序的必需元素。控制器通过 rememberNavController() 创建,传递给 NavHost,可从组合中的任意点调用 navigate()。内置的 SavedStateHandle 支持在重新配置时自动保存 ViewModel 状态。

要点

  • NavController — Compose 导航的中心控制器,管理 back stack 和屏幕之间的切换
  • navigate() — 切换到路由的主要方法,支持 NavOptions 进行堆栈管理
  • popBackStack() — 返回上一屏幕,可选择清除到指定路由
  • SavedStateHandle — 与 ViewModel 集成,在导航时保存屏幕状态
  • currentBackStackEntryAsState() — 观察当前路由以同步 UI

什么是 Jetpack Compose 中的 NavController?

NavController — 来自 Navigation Compose 库的一个类,为 Compose 应用程序实现导航控制器。NavController 管理 NavBackStackEntry 堆栈,其中每个条目包含路由、参数和屏幕状态。控制器支持基本导航操作:切换、返回、替换和清除。

与通过 FragmentManager 或 Intent 进行导航的 View 系统不同,NavController 仅在 Compose 上下文中工作。Back stack 存储为 NavDestination 图,而不是 Fragment 堆栈。这消除了创建和销毁 Fragment 的开销,并简化了测试——NavController 可以通过 TestNavHostController 进行模拟。

NavController 与 NavHost 紧密相关——从图中渲染当前屏幕的容器。没有 NavHost,NavController 无法显示 composable 函数,但保留管理堆栈的能力。在典型架构中,NavController 在 Activity 或主 composable 级别创建,并通过参数沿组合树向下传递。

根据 Google 的资料,NavController 经历了多个主要版本。2.8.0 版本添加了 Type-Safe Navigation,2.9.0 版本——支持 predictive back gesture(Android 14+)。该控制器与 Material3 Scaffold 和 BottomNavigation 兼容。对于多模块项目,NavController 通过 DI(Hilt/Koin)或构造函数参数传递。

NavController 通过 rememberNavController() composable 函数创建。该函数返回一个 NavHostController 实例(NavController 的子类),与当前 composable 的生命周期绑定。退出组合时,控制器被清除。要在重新配置时保存控制器,请使用 rememberSaveable 或 ViewModel。

kotlin
@Composable
fun MyApp() {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = "main"
    ) {
        composable("main") { MainScreen(navController) }
        composable("details") { DetailsScreen(navController) }
    }
}

NavController 的配置包括:NavHostController(主要)、TestNavHostController(测试)和 ScopedNavController(用于嵌套图的子控制器)。对于 BottomNavigation,NavController 应对整个应用程序唯一——在每个选项卡中创建新控制器将导致堆栈丢失。要将控制器传递给嵌套屏幕,请使用函数参数而不是 CompositionLocalProvider,以保持可读性。

要测试导航,请使用带有 compose-test-rule 的 TestNavHostController。该控制器允许设置初始路由并验证 navigate() 是否调用了预期的切换。NavController 的测试不需要模拟器——它与 Compose Test 的 Semantics 匹配器一起工作。

navigate(route: String) 方法——NavController 中导航的主要方式。接受路由字符串、可选的 NavOptions 和 Navigator.Extras。NavOptions 管理切换行为:launchSingleTop(不在堆栈中重复路由)、popUpTo(清除堆栈到路由)、restoreState(恢复先前状态)。

NavOptions 通过构建器语法设置:NavOptionsBuilder。主要参数:popUpTo(route + inclusive/saveState)、launchSingleTop(Boolean,true——不创建重复项)、restoreState(返回时恢复状态)。没有 popUpTo,每个 navigate() 都会向堆栈添加条目,导致 back stack 累积和 Back 按钮行为不正确。

kotlin
navController.navigate("profile/42") {
    popUpTo("main") { saveState = true }
    launchSingleTop = true
    restoreState = true
}

Navigator.Extras 允许传递不属于路由的额外数据:用于动画的共享元素、Intent 标志、Pac-Man bundle。Extras 很少使用——主要用于与 Accompanist Animation 或自定义 Navigator 的集成。对于大多数场景,路由字符串和 NavOptions 就足够了。

popBackStack:管理返回和堆栈清除

popBackStack() — 返回上一屏幕的方法。无参数时删除堆栈顶部条目并返回 true(如果删除成功)。如果堆栈为空——该方法返回 false,Activity 关闭(类似于 super.onBackPressed())。

重载版本 popBackStack(route: String, inclusive: Boolean) 删除直到指定路由的所有条目。如果 inclusive = true——指定的路由本身也被删除。该方法返回 Boolean——如果找到并删除了条目则返回 true。带有 inclusive 的版本适用于授权或订单完成后“退出到主屏幕”的场景。

方法描述示例
popBackStack()向后返回一个屏幕navController.popBackStack()
popBackStack(route, false)清除到路由(路由保留)popBackStack("home", false)
popBackStack(route, true)清除到并包括路由popBackStack("home", true)
navigate(route) { popUpTo(route) { inclusive = true } }完全清除的切换navigate("login") { popUpTo(0) { inclusive = true } }

要处理系统 Back 按钮(硬件返回按钮),请使用 Compose 中的 BackHandler。BackHandler 接受 enabled 和 onBack——按下时调用的回调。对于 Android 14+,使用 PredictiveBackGesture,通过 NavController 2.9.0 版本集成。Predictive back 添加了返回预览动画。

SavedStateHandle:保存屏幕状态

SavedStateHandle — 一种在导航和重新配置时保存 ViewModel 状态的机制。NavController 自动为每个 NavBackStackEntry 提供 SavedStateHandle。通过 SavedStateHandle,ViewModel 存储屏幕状态并在返回时恢复(restoreState = true)。

在 Navigation Compose 中,SavedStateHandle 与 ViewModel 一起使用:ViewModel 通过从 backStackEntry 传递的 SavedStateHandle 进行初始化。在切换到另一个屏幕并返回时(使用 restoreState),ViewModel 接收保存的状态,而不是重新创建。这对于具有数据输入、过滤器或滚动的屏幕至关重要。

kotlin
class ProfileViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    val userId: String = savedStateHandle.get<String>("userId") ?: ""
    var searchQuery by savedStateHandle.getStateFlow("search", "")
        .collectAsState()
}

SavedStateHandle 支持原始类型、String、Bundle 和 Parcelable。对于复杂对象,只保存 ID,完整数据从存储库加载。SavedStateHandle 的限制约为 1 MB,超出会导致 TransactionTooLargeException。对于大量数据,请使用 Room 或 DataStore 而不是在 handle 中保存。

重要提示:SavedStateHandle 仅在使用 NavOptions 中的 restoreState = true 时保存状态。如果未指定 restoreState,返回时 ViewModel 将使用默认值重新创建。对于使用 restoreState 切换 BottomNavigation,NavController 保存每个选项卡的状态并在重新选择时恢复。

通过 currentBackStackEntryAsState 观察当前路由

currentBackStackEntryAsState() — 返回 State<NavBackStackEntry?> 的函数,该状态在当前路由每次更改时更新。这是将 UI 与导航同步的主要机制:BottomNavigation 突出显示活动元素,Toolbar 更新标题,Drawer 在切换时关闭。

该函数通过 snapshotFlow 和 collectAsState 工作:当 back stack 更改时,Compose 重新组合订阅的元素。重要提示:currentBackStackEntryAsState() 仅在切换动画完成后更新。要立即更新,请使用 currentDestination,它与 navigate() 同步更改,但不支持状态。

kotlin
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route

Text(
    text = when (currentRoute) {
        "home" -> "Home"
        "profile" -> "Profile"
        else -> ""
    }
)

要访问当前路由的参数,请使用 navBackStackEntry?.arguments。这在 BottomNavigation 中很方便:selectedItem 基于 currentRoute 计算。要调试导航,请使用 NavController.addOnDestinationChangedListener(),它会记录每次切换。在生产中,避免在大量 composable 中进行订阅——在 ViewModel 中创建单一源并将 State 传递给 UI。

常见问题

可以在一个 Activity 中创建多个 NavController 吗?

技术上可以,但不建议。单个 NavController 确保一致的 back stack 并简化调试。多个控制器仅对于具有独立导航的嵌套图是合理的(例如,具有自己堆栈的 modal bottom sheet)。

如何通过 ViewModel 传递 NavController?

通过构造函数或 DI 将 NavController 传递给 ViewModel。但是,最好只传递回调函数(onNavigate、onBack),而不是 NavController 本身——这简化了测试。对于事件,在 ViewModel 中使用 Channel<NavEvent> 并在 UI 中收集。

为什么 navigate 在异步操作后不起作用?

问题在于生命周期:如果 NavController 尚未初始化(NavHost 尚未构建),navigate() 将被忽略。使用 LaunchedEffect 在数据加载后调用导航,而不是在具有任意生命周期协程内部。

如何清除整个 back stack 并转到新屏幕?

调用 navController.navigate("target") { popUpTo(0) { inclusive = true } }。参数 popUpTo(0) 完全清除堆栈,inclusive = true 也删除初始条目。标志 launchSingleTop = true 防止新路由重复。

NavHostController 和 NavController 有什么区别?

NavHostController — NavController 的子类,具有用于 NavHost 的额外方法(例如 setOnBackStackChangedListener)。NavController — 基类,可在 NavHost 之外用于程序化堆栈管理。在大多数情况下,使用 NavHostController。

总结

  • NavController — Navigation Compose 的核心组件,管理路由堆栈和屏幕之间的切换
  • navigate() 通过 NavOptions 使用 popUpTo、launchSingleTop 和 restoreState 设置执行切换
  • popBackStack() 管理返回:单步或批量清除到带有 inclusive 的指定路由
  • SavedStateHandle 与 ViewModel 集成,在导航时自动保存屏幕状态
  • currentBackStackEntryAsState() 提供当前路由的响应式观察以同步 UI
  • BackHandler 处理系统 Back 按钮,PredictiveBackGesture 从 NavController 2.9.0 开始支持
  • 对于测试,使用带有 compose-test-rule 和 Semantics 匹配器的 TestNavHostController

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读