使用 Compose 进行导航

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

设置

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

Groovy

dependencies {
    def nav_version = "2.8.0"

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

Kotlin

dependencies {
    val nav_version = "2.8.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.7.0"
}

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.15"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

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

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标志,在您在底部导航项目之间切换时,该项目的状体和回退堆栈会正确保存和恢复。

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 一起使用,则有两个选项。

  • 使用导航组件为片段定义导航图。
  • 使用 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本身。

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

@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的实例来测试AppNavHostNavHost内部定义的所有导航逻辑。验证应用和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 应用。

示例