导航组件为 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") }
开始
在应用中实现导航时,请实现导航宿主、图和控制器。有关更多信息,请参阅导航概览。
创建 NavController
有关如何在 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
中检索参数,请使用 ViewModel
的 SavedStateHandle
。
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
,您可以将导航与其他组件(例如底部导航组件)连接起来。这样做可以通过选择底部栏中的图标进行导航。
要使用 BottomNavigation
和 BottomNavigationItem
组件,请将 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
的调用,以便点击该项目时导航到该项目。通过使用 saveState
和 restoreState
标志,当您在底部导航项目之间切换时,该项目的状态和返回堆栈会正确保存和恢复。
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 使用 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
通过比较当前路由与预期路由来检查断言,使用 navController
的 currentBackStackEntry
@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 应用。
示例
为您推荐
- 注意:当 JavaScript 关闭时,会显示链接文本
- Compose 中的 Material Design 2
- 将 Jetpack Navigation 迁移到 Navigation Compose
- 状态提升的位置