使用 Compose 进行导航

导航组件为 Jetpack Compose 应用提供支持。您可以在可组合项之间进行导航,同时利用导航组件的基础设施和功能。

有关专门为 Compose 构建的最新 Alpha 导航库,请参阅 导航 3 文档

设置

为了支持 Compose,请在您的应用模块的 build.gradle 文件中使用以下依赖项

Groovy

dependencies {
    def nav_version = "2.9.0"

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

Kotlin

dependencies {
    val nav_version = "2.9.0"

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

开始

在应用中实现导航时,请实现导航宿主、图和控制器。有关更多信息,请参阅导航概览。

有关如何在 Compose 中创建 NavController 的信息,请参阅创建导航控制器的 Compose 部分。

创建 NavHost

有关如何在 Compose 中创建 NavHost 的信息,请参阅设计您的导航图的 Compose 部分。

有关导航到可组合项的信息,请参阅架构文档中的导航到目标

有关在可组合目标之间传递参数的信息,请参阅设计您的导航图的 Compose 部分。

导航时检索复杂数据

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

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

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

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

    private val profile = savedStateHandle.toRoute<Profile>()

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

// …

}

这种方法有助于防止配置更改期间的数据丢失,并避免在相关对象更新或修改时出现任何不一致。

有关为何应避免将复杂数据作为参数传递,以及支持的参数类型列表的更深入解释,请参阅在目标之间传递数据

Navigation Compose 也支持深度链接,它们可以作为 composable() 函数的一部分进行定义。其 deepLinks 参数接受一个 NavDeepLink 对象列表,这些对象可以使用 navDeepLink() 方法快速创建。

@Serializable data class Profile(val id: String)
val uri = "https://www.example.com"

composable<Profile>(
  deepLinks = listOf(
    navDeepLink<Profile>(basePath = "$uri/profile")
  )
) { backStackEntry ->
  ProfileScreen(id = backStackEntry.toRoute<Profile>().id)
}

这些深度链接允许您将特定的 URL、操作或 MIME 类型与可组合项关联起来。默认情况下,这些深度链接不会暴露给外部应用。要使这些深度链接可供外部访问,您必须将适当的 <intent-filter> 元素添加到应用的 manifest.xml 文件中。要在上述示例中启用深度链接,您应在清单的 <activity> 元素内添加以下内容

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

当深度链接由另一个应用触发时,导航会自动深度链接到该可组合项。

这些深度链接也可以用于从可组合项构建一个带有适当深度链接的 PendingIntent

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

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

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

嵌套导航

有关如何创建嵌套导航图的信息,请参阅嵌套图

与底部导航栏集成

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

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

Groovy

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

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.15"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

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

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.15"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

要将底部导航栏中的项目链接到导航图中的路由,建议定义一个类,例如此处所示的 TopLevelRoute,该类包含路由类和图标。

data class TopLevelRoute<T : Any>(val name: String, val route: T, val icon: ImageVector)

然后将这些路由放置在一个列表中,供 BottomNavigationItem 使用。

val topLevelRoutes = listOf(
   TopLevelRoute("Profile", Profile, Icons.Profile),
   TopLevelRoute("Friends", Friends, Icons.Friends)
)

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

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

val navController = rememberNavController()
Scaffold(
  bottomBar = {
    BottomNavigation {
      val navBackStackEntry by navController.currentBackStackEntryAsState()
      val currentDestination = navBackStackEntry?.destination
      topLevelRoutes.forEach { topLevelRoute ->
        BottomNavigationItem(
          icon = { Icon(topLevelRoute.icon, contentDescription = topLevelRoute.name) },
          label = { Text(topLevelRoute.name) },
          selected = currentDestination?.hierarchy?.any { it.hasRoute(topLevelRoute.route::class) } == true,
          onClick = {
            navController.navigate(topLevelRoute.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 = Profile, Modifier.padding(innerPadding)) {
    composable<Profile> { ProfileScreen(...) }
    composable<Friends> { FriendsScreen(...) }
  }
}

您在这里利用 NavController.currentBackStackEntryAsState() 方法将 navController 状态从 NavHost 函数中提升出来,并与 BottomNavigation 组件共享。这意味着 BottomNavigation 自动拥有最新的状态。

互操作性

如果您想将导航组件与 Compose 结合使用,您有两种选择

  • 使用导航组件为 Fragment 定义导航图。
  • 在 Compose 中使用 Compose 目标定义带有 NavHost 的导航图。这仅在导航图中的所有屏幕都是可组合项时才可能实现。

因此,对于混合使用 Compose 和 View 的应用,建议使用基于 Fragment 的导航组件。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 的导航组件之间建立桥梁。

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

或者,您可以将 NavController 传递到您的 Compose 层次结构中。但是,公开简单的函数更具可重用性和可测试性。

测试

将导航代码与您的可组合目标解耦,以便独立测试每个可组合项,与 NavHost 可组合项分离。

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

composable lambda 提供的间接层使您能够将导航代码与可组合项本身分离。这在两个方向上都适用

  • 仅将已解析的参数传递到您的可组合项中
  • 传递应由可组合项触发以进行导航的 lambda,而不是 NavController 本身。

例如,一个 ProfileScreen 可组合项,它接收 userId 作为输入并允许用户导航到朋友的资料页面,其签名可能如下

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

这样,ProfileScreen 可组合项独立于导航工作,从而可以独立进行测试。composable lambda 将封装连接导航 API 和您的可组合项之间所需的最小逻辑。

@Serializable data class Profile(id: String)

composable<Profile> { backStackEntry ->
    val profile = backStackEntry.toRoute<Profile>()
    ProfileScreen(userId = profile.id) { friendUserId ->
        navController.navigate(route = Profile(id = friendUserId))
    }
}

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

测试 NavHost

要开始测试您的 NavHost,请添加以下导航测试依赖项

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

将应用的 NavHost 封装在一个可组合项中,该可组合项接受 NavHostController 作为参数。

@Composable
fun AppNavHost(navController: NavHostController){
  NavHost(navController = navController){ ... }
}

现在,您可以通过传递导航测试构件 TestNavHostController 的实例来测试 AppNavHost 以及在 NavHost 内部定义的所有导航逻辑。一个验证应用和 NavHost 起始目标的 UI 测试将如下所示

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()
    }
}

测试导航操作

您可以通过多种方式测试您的导航实现:通过点击 UI 元素,然后验证显示的目标,或者将预期路由与当前路由进行比较。

由于您想测试具体的应用实现,因此点击 UI 是首选。要了解如何将此与独立的单个可组合函数一起测试,请务必查看 Jetpack Compose 中的测试 Codelab。

您还可以使用 navController 通过比较当前路由与预期路由来检查断言,使用 navControllercurrentBackStackEntry

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

    assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}

有关 Compose 测试基础知识的更多指导,请参阅测试您的 Compose 布局Jetpack Compose 中的测试 Codelab。要了解有关导航代码高级测试的更多信息,请访问测试导航指南。

了解更多

要了解有关 Jetpack Navigation 的更多信息,请参阅导航组件入门或参加 Jetpack Compose Navigation Codelab

要了解如何设计应用导航以适应不同的屏幕尺寸、方向和外形尺寸,请参阅响应式 UI 的导航

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

示例