使用 Compose 进行导航

导航组件 为 Jetpack Compose 应用程序提供支持。您可以利用导航组件的基础架构和功能在可组合项之间导航。

设置

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

Groovy

dependencies {
    def nav_version = "2.8.4"

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

Kotlin

dependencies {
    val nav_version = "2.8.4"

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

开始使用

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

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

创建 NavHost

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

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

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

导航时检索复杂数据

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

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

复杂的 objects 应存储为单个 truth 数据源中的数据,例如数据层。导航后到达目标后,您可以使用传递的 ID 从单个 truth 数据源加载所需的信息。要检索负责访问数据层的 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.7.5"
}

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.15"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

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

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。然后,可以通过将项目的路线与当前目标的路线及其父目标的路线进行比较来确定每个 BottomNavigationItem 的选中状态,以便处理使用 嵌套导航 时的情况,方法是使用 NavDestination 层次结构。

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

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 state 从 NavHost 函数中提升出来,并与 BottomNavigation 组件共享。这意味着 BottomNavigation 会自动拥有最新的 state。

互操作性

如果您想将导航组件与 Compose 一起使用,则有两种选择:

  • 使用导航组件为片段定义导航图。
  • 使用 Compose 目标在 Compose 中使用 NavHost 定义导航图。只有当导航图中的所有屏幕都是可组合项时,才有可能。

因此,对于混合 Compose 和视图应用的建议是使用基于片段的导航组件。然后,片段将保存基于视图的屏幕、Compose 屏幕以及同时使用视图和 Compose 的屏幕。一旦每个片段的内容都在 Compose 中,下一步就是使用 Navigation Compose 将所有这些屏幕绑定在一起,并删除所有片段。

为了更改 Compose 代码内的目标,您可以公开可以由层次结构中任何可组合项传递和触发的事件。

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

在您的片段中,您可以通过查找 NavController 并导航到目标来在 Compose 和基于片段的导航组件之间建立桥梁。

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

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

测试

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

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

composable lambda 提供的间接级别使您可以将导航代码与可组合项本身分开。这双向有效:

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

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

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

通过这种方式, ProfileScreen 可组合项独立于 Navigation 工作,允许独立测试它。 composable lambda 将封装桥接 Navigation 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 测试 代码实验室。

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

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

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

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

了解更多

要了解有关Jetpack Navigation的更多信息,请参阅开始使用导航组件或参加Jetpack Compose 导航代码实验室

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

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

示例