使用 Compose 进行导航

Navigation 组件支持 Jetpack Compose 应用。您可以在利用 Navigation 组件的基础架构和功能的同时,在可组合项之间导航。

设置

如需支持 Compose,请在应用模块的 build.gradle 文件中使用以下依赖项:

Groovy

dependencies {
    def nav_version = "2.7.7"

    implementation "androidx.navigation:navigation-compose:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.7.7"

    implementation("androidx.navigation:navigation-compose:$nav_version")
}

开始使用

在应用中实现导航时,实现导航宿主、图和控制器。如需了解详情,请参阅导航概览。

如需了解如何在 Compose 中创建 NavController,请参阅创建导航控制器的 Compose 部分。

创建 NavHost

如需了解如何在 Compose 中创建 NavHost,请参阅设计导航图的 Compose 部分。

如需了解如何导航到可组合项,请参阅架构文档中的导航到目的地

Navigation Compose 还支持在可组合项目的地之间传递参数。为此,您需要向路线中添加参数占位符,就像在使用基础导航库时向深层链接中添加参数一样。

NavHost(startDestination = "profile/{userId}") {
    ...
    composable("profile/{userId}") {...}
}

默认情况下,所有参数都会被解析为字符串。composable()arguments 参数接受 NamedNavArgument 对象的列表。您可以使用 navArgument() 方法快速创建 NamedNavArgument,然后指定其确切 type

NavHost(startDestination = "profile/{userId}") {
    ...
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}

您应该从 composable() 函数的 lambda 中提供的 NavBackStackEntry 中提取这些参数。

composable("profile/{userId}") { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

若要将相应参数传递到目的地,您需要在执行 navigate 调用时将该参数添加到路线:

navController.navigate("profile/user1234")

如需查看支持的类型的列表,请参阅在目的地之间传递数据

在导航时检索复杂数据

强烈建议在导航时不要传递复杂的数据对象,而是在执行导航操作时将最少的必要信息(例如唯一标识符或其他形式的 ID)作为参数传递:

// Pass only the user ID when navigating to a new destination as argument
navController.navigate("profile/user1234")

复杂对象应以数据存储在单一可信来源(例如数据层)中。在导航后到达目的地后,您可以使用所传递的 ID 从单一可信来源加载所需信息。如需检索 ViewModel 中负责访问数据层的参数,请使用 ViewModelSavedStateHandle

class UserViewModel(
    savedStateHandle: SavedStateHandle,
    private val userInfoRepository: UserInfoRepository
) : ViewModel() {

    private val userId: String = checkNotNull(savedStateHandle["userId"])

    // Fetch the relevant user information from the data layer,
    // ie. userInfoRepository, based on the passed userId argument
    private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(userId)

// …

}

这种方法有助于防止配置更改期间发生数据丢失,以及在更新或更改相关对象时造成任何不一致。

如需深入了解为何应避免将复杂数据作为参数传递,以及支持的参数类型列表,请参阅在目的地之间传递数据

添加可选参数

Navigation Compose 还支持可选的导航参数。可选参数与必需参数有以下两点不同:

  • 可选参数必须使用查询参数语法 ("?argName={argName}") 来添加
  • 可选参数必须具有 defaultValue 集或 nullable = true(将默认值隐式设置为 null

这意味着,所有可选参数都必须以列表的形式显式添加到 composable() 函数:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

现在,即使没有向目的地传递任何参数,系统也会使用 defaultValue“user1234”。

通过路线处理参数的结构意味着可组合项将完全独立于 Navigation,并且更易于测试。

Navigation Compose 支持隐式深层链接,此类链接也可定义为 composable() 函数的一部分。其 deepLinks 参数接受 NavDeepLink 对象列表,您可以使用 navDeepLink() 方法快速创建这些对象:

val uri = "https://www.example.com"

composable(
    "profile?id={id}",
    deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("id"))
}

通过这些深层链接,您可以将特定网址、操作或 MIME 类型与可组合项相关联。默认情况下,这些深层链接不会向外部应用公开。如需向外部提供这些深层链接,您必须将相应的 <intent-filter> 元素添加到应用的 manifest.xml 文件中。如需启用上述示例中的深层链接,您应在清单的 <activity> 元素中添加以下内容:

<activity …>
  <intent-filter>
    ...
    <data android:scheme="https" android:host="www.example.com" />
  </intent-filter>
</activity>

当其他应用触发该深层链接时,Navigation 会自动深层链接到相应的可组合项。

这些深层链接还可用于构建包含可组合项中的相关深层链接的 PendingIntent

val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://www.example.com/$id".toUri(),
    context,
    MyActivity::class.java
)

val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
    addNextIntentWithParentStack(deepLinkIntent)
    getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}

然后,您可以像使用任何其他 PendingIntent 一样,使用此 deepLinkPendingIntent 在相应深层链接目的地打开您的应用。

嵌套导航结构

如需了解如何创建嵌套导航图,请参阅嵌套图

与底部导航栏集成

通过在可组合项层次结构中的更高层级定义 NavController,您可以将 Navigation 与其他组件(例如底部导航组件)相关联。这样,您就可以通过选择底部栏中的图标来进行导航。

如需使用 BottomNavigationBottomNavigationItem 组件,请将 androidx.compose.material 依赖项添加到您的 Android 应用中。

Groovy

dependencies {
    implementation "androidx.compose.material:material:1.6.3"
}

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.10"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

dependencies {
    implementation("androidx.compose.material:material:1.6.3")
}

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.10"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

如需将底部导航栏中的项与您的导航图中的路线相关联,建议您定义密封的类(例如此处所示的 Screen),其中包含相应目的地的路线和字符串资源 ID。

sealed class Screen(val route: String, @StringRes val resourceId: Int) {
    object Profile : Screen("profile", R.string.profile)
    object FriendsList : Screen("friendslist", R.string.friends_list)
}

然后,将这些项放置在 BottomNavigationItem 可以使用的列表中:

val items = listOf(
   Screen.Profile,
   Screen.FriendsList,
)

BottomNavigation 可组合项中,使用 currentBackStackEntryAsState() 函数获取当前的 NavBackStackEntry。此条目允许您访问当前的 NavDestination。然后,可通过将该项的路由与当前目的地及其父目的地的路由进行比较来确定每个 BottomNavigationItem 的选定状态,以处理使用 NavDestination 层次结构的嵌套导航的情况。

该项目的路由还用于将 onClick lambda 连接到对 navigate 的调用,以便在点按该项时会转到该项。通过使用 saveStaterestoreState 标志,当您在底部导航项之间切换时,系统会正确保存并恢复该项的状态和返回堆栈。

val navController = rememberNavController()
Scaffold(
  bottomBar = {
    BottomNavigation {
      val navBackStackEntry by navController.currentBackStackEntryAsState()
      val currentDestination = navBackStackEntry?.destination
      items.forEach { screen ->
        BottomNavigationItem(
          icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
          label = { Text(stringResource(screen.resourceId)) },
          selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
          onClick = {
            navController.navigate(screen.route) {
              // Pop up to the start destination of the graph to
              // avoid building up a large stack of destinations
              // on the back stack as users select items
              popUpTo(navController.graph.findStartDestination().id) {
                saveState = true
              }
              // Avoid multiple copies of the same destination when
              // reselecting the same item
              launchSingleTop = true
              // Restore state when reselecting a previously selected item
              restoreState = true
            }
          }
        )
      }
    }
  }
) { innerPadding ->
  NavHost(navController, startDestination = Screen.Profile.route, Modifier.padding(innerPadding)) {
    composable(Screen.Profile.route) { Profile(navController) }
    composable(Screen.FriendsList.route) { FriendsList(navController) }
  }
}

在这里,您可以利用 NavController.currentBackStackEntryAsState() 方法从 NavHost 函数中获取 navController 状态,并与 BottomNavigation 组件共享此状态。这意味着 BottomNavigation 会自动拥有最新状态。

Navigation Compose 中的类型安全

本页面上的代码不符合类型安全要求。您可能使用无效路由或错误的参数调用了 navigate() 函数。不过,您可以设计 Navigation 代码的结构,使其在运行时符合类型安全要求。这样做可以避免崩溃,并确保:

  • 导航到目的地或导航图时您提供的参数的类型正确,并且所需的所有参数都存在。
  • 您从 SavedStateHandle 中检索到的参数的类型正确。

如需了解详情,请参阅 Kotlin DSL 和 Navigation Compose 中的类型安全

互操作性

如果您想将 Navigation 组件与 Compose 配合使用,有以下两种选择:

  • 使用基于 fragment 的 Navigation 组件定义导航图。
  • 使用 Compose 目的地在 Compose 中通过 NavHost 定义导航图。只有在导航图中的所有界面都是可组合项的情况下,才可以这么做。

因此,若要构建 Compose 和 View 混合应用,我们建议使用基于 Fragment 的 Navigation 组件。然后,fragment 将保存基于 View 的界面、Compose 界面,以及同时使用 View 和 Compose 的界面。每个 fragment 的内容都添加到 Compose 后,下一步就是将所有这些界面与 Navigation Compose 结合在一起,并移除所有 fragment。

要在 Compose 代码内更改目的地,您可以公开可传递到并由层次结构中的任何可组合项触发的事件:

@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
    Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}

在您的 fragment 中,您可以通过找到 NavController 并导航到目的地,在 Compose 和基于 fragment 的 Navigation 组件之间架起桥梁:

override fun onCreateView( /* ... */ ) {
    setContent {
        MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
    }
}

或者,您可以将 NavController 传递到 Compose 层次结构下方。不过,公开简单的函数的可重用性和可测试性更高。

测试

将导航代码与可组合项目的地分离,以便独立于 NavHost 可组合项单独测试每个可组合项。

这意味着,您不应将 navController 直接传递到任何可组合项中,而不应将导航回调作为参数传递。这样一来,您的所有可组合项均可单独测试,因为它们不需要在测试中使用 navController 实例。

借助 composable lambda 提供的间接层,您可以将 Navigation 代码与可组合项本身分离开。这在以下两个方向上可行:

  • 仅将解析后的参数传递到可组合项
  • 传递应由要导航的可组合项触发的 lambda,而不是 NavController 本身。

例如,如果某个 Profile 可组合项接受 userId 作为输入并允许用户导航到好友的个人资料页面,则可以采用以下签名:

@Composable
fun Profile(
    userId: String,
    navigateToFriendProfile: (friendUserId: String) -> Unit
) {
 …
}

这样,Profile 可组合项可独立于 Navigation 运行,从而可单独进行测试。composable lambda 会封装弥合 Navigation API 与您的可组合项之间的差距所需的基本逻辑:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
    Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
        navController.navigate("profile?userId=$friendUserId")
    }
}

建议通过测试 NavHost、传递给可组合项的导航操作以及各个屏幕可组合项来编写涵盖应用导航要求的测试。

测试 NavHost

如需开始测试 NavHost,请添加以下导航测试依赖项:

dependencies {
// ...
  androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
  // ...
}

您可以设置 NavHost 测试对象,并将 navController 实例的实例传递给该对象。为此,导航测试工件提供了一个 TestNavHostController。用于验证应用启动目的地和 NavHost 的界面测试将如下所示:

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: TestNavHostController

    @Before
    fun setupAppNavHost() {
        composeTestRule.setContent {
            navController = TestNavHostController(LocalContext.current)
            navController.navigatorProvider.addNavigator(ComposeNavigator())
            AppNavHost(navController = navController)
        }
    }

    // Unit test
    @Test
    fun appNavHost_verifyStartDestination() {
        composeTestRule
            .onNodeWithContentDescription("Start Screen")
            .assertIsDisplayed()
    }
}

测试导航操作

您可以通过多种方式测试导航实现,方法是对界面元素执行点击,然后验证所显示的目的地,或将预期路线与当前路线进行比较。

如果您要测试具体应用的实现,最好在界面中执行点击操作。如需了解如何单独测试各个可组合函数,请务必查看在 Jetpack Compose 中进行测试 Codelab。

您还可以使用 navController 检查您的断言,只需使用 navControllercurrentBackStackEntry 将当前 String 路线与预期路线进行比较即可:

@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
    composeTestRule.onNodeWithContentDescription("All Profiles")
        .performScrollTo()
        .performClick()

    val route = navController.currentBackStackEntry?.destination?.route
    assertEquals(route, "profiles")
}

如需详细了解 Compose 测试基础知识,请参阅测试 Compose 布局在 Jetpack Compose 中进行测试 Codelab。 如需详细了解导航代码的高级测试,请参阅测试导航指南。

了解详情

如需详细了解 Jetpack Navigation,请参阅 Navigation 组件使用入门或完成 Jetpack Compose Navigation Codelab

如需了解如何设计应用导航,使其适应不同的屏幕尺寸、屏幕方向和设备外形规格,请参阅自适应界面的导航

如需了解模块化应用中的更高级 Compose 导航实现,包括嵌套图和底部导航栏集成等概念,请查看 GitHub 上的 Now in Android 应用。

示例