Navigation 组件支持 Jetpack Compose 应用。您可以在利用 Navigation 组件的基础架构和功能的同时,在可组合项之间导航。
设置
如需支持 Compose,请在应用模块的 build.gradle
文件中使用以下依赖项:
Groovy
dependencies { def nav_version = "2.5.3" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { def nav_version = "2.5.3" implementation("androidx.navigation:navigation-compose:$nav_version") }
使用入门
NavController
是 Navigation 组件的中心 API。此 API 是有状态的,可以跟踪组成应用屏幕的可组合项的返回堆栈以及每个屏幕的状态。
您可以通过在可组合项中使用 rememberNavController()
方法来创建 NavController
:
val navController = rememberNavController()
您应该在可组合项层次结构中的适当位置创建 NavController
,使所有需要引用它的可组合项都可以访问它。这遵循状态提升的原则,并且允许您使用 NavController
及其通过 currentBackStackEntryAsState()
提供的状态作为更新屏幕外的可组合项的可信来源。有关此功能的示例,请参阅与底部导航栏集成。
创建 NavHost
每个 NavController
都必须与一个 NavHost
可组合项相关联。NavHost
将 NavController
与导航图相关联,后者用于指定您应能够在其间进行导航的可组合项目的地。当您在可组合项之间进行导航时,NavHost
的内容会自动进行重组。导航图中的每个可组合项目的地都与一个路线相关联。
如需创建 NavHost
,您需要使用之前通过 rememberNavController()
创建的 NavController
,以及导航图的起始目的地的路线。NavHost
创建使用 Navigation Kotlin DSL 中的 lambda 语法来构建导航图。您可以使用 composable()
方法向导航结构添加内容。此方法需要您提供一个路线以及应关联到相应目的地的可组合项:
NavHost(navController = navController, startDestination = "profile") {
composable("profile") { Profile(/*...*/) }
composable("friendslist") { FriendsList(/*...*/) }
/*...*/
}
导航到可组合项
如需导航到导航图中的可组合项目的地,您必须使用 navigate
方法。navigate
接受代表目的地路线的单个 String
参数。如需从导航图中的某个可组合项进行导航,请调用 navigate
:
navController.navigate("friendslist")
默认情况下,navigate
会将您的新目的地添加到返回堆栈中。您可以通过向我们的 navigate()
调用附加其他导航选项来修改 navigate
的行为:
// Pop everything up to the "home" destination off the back stack before
// navigating to the "friendslist" destination
navController.navigate("friendslist") {
popUpTo("home")
}
// Pop everything up to and including the "home" destination off
// the back stack before navigating to the "friendslist" destination
navController.navigate("friendslist") {
popUpTo("home") { inclusive = true }
}
// Navigate to the "search” destination only if we’re not already on
// the "search" destination, avoiding multiple copies on the top of the
// back stack
navController.navigate("search") {
launchSingleTop = true
}
如需查看更多用例,请参阅 popUpTo 指南。
导航由其他可组合函数触发的调用
NavController
的 navigate
函数会修改 NavController
的内部状态。为了尽可能符合单一可信来源原则,只有提升 NavController
实例的可组合函数或状态容器,以及接受 NavController
作为参数的可组合函数才应进行导航调用。从界面层次结构中处于较低层级的其他可组合函数触发的导航事件需要使用函数将相应事件适当地公开给调用方。
以下示例显示了作为 NavController
实例的单一可信来源的 MyAppNavHost
可组合函数。ProfileScreen
将一个事件公开为用户点按按钮时调用的函数。MyAppNavHost
负责前往应用中的不同屏幕,它会在调用 ProfileScreen
时对正确的目的地进行导航调用。
@Composable
fun MyAppNavHost(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
startDestination: String = "profile"
) {
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable("profile") {
ProfileScreen(
onNavigateToFriends = { navController.navigate("friendsList") },
/*...*/
)
}
composable("friendslist") { FriendsListScreen(/*...*/) }
}
}
@Composable
fun ProfileScreen(
onNavigateToFriends: () -> Unit,
/*...*/
) {
/*...*/
Button(onClick = onNavigateToFriends) {
Text(text = "See friends list")
}
}
您应仅在回调中调用 navigate()
,而不能在可组合项本身中调用它,以避免每次重组时都调用 navigate()
。
最佳实践
在 Compose 中提升状态时的一项最佳实践是,将可组合函数中的事件公开给知道如何处理应用中的特定逻辑的调用方。
虽然将事件作为单独的 lambda 参数公开可能会使函数签名过载,但它可以最大限度提高可组合函数功能的可见性。您可以一目了然地了解它的功能。
其他可以减少函数声明中的参数数量的替代方案最初可能更易于编写,但从长远来看会隐藏一些缺陷。例如,创建一个像 ProfileScreenEvents
一样的封装容器类,将所有事件集中在一处。这样做会降低可组合项在执行其函数定义时的功能的可见性,它会将其他类和方法加入项目计数,并且无论如何,您每次调用此可组合函数时都需要创建并记住相应类的实例。此外,为了尽可能重复使用相应封装容器类,这种模式还会鼓励根据界面层次结构向下传递该类的实例,而不是按照最佳实践(即仅向可组合项传递其所需的内容)的要求操作。
使用参数进行导航
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 中负责访问数据层的参数,您可以使用 ViewModel’s
SavedStateHandle
:
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
集或nullability = 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 类型与可组合项关联起来。默认情况下,这些深层链接不会向外部应用公开。如需向外部提供这些深层链接,您必须向应用的 manifest.xml
文件添加相应的 <intent-filter>
元素。如需启用上述深层链接,您应该在清单的 <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
在相应深层链接目的地打开您的应用。
嵌套导航结构
您可以将目的地归入一个嵌套图,以便在应用界面中对特定流程进行模块化。例如,您可以对独立的登录流程进行模块化。
嵌套图可以封装其目的地。与根图一样,嵌套图必须具有被其路径标识为起始目的地的目的地。此目的地是当您导航到与嵌套图相关联的路径时所导航到的目的地。
如需向 NavHost
添加嵌套图,您可以使用 navigation
扩展函数:
NavHost(navController, startDestination = "home") {
...
// Navigating to the graph via its route ('login') automatically
// navigates to the graph's start destination - 'username'
// therefore encapsulating the graph's internal routing logic
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
...
}
强烈建议您在导航图变大时将其拆分为多个方法。这也允许多个模块提交各自的导航图。
fun NavGraphBuilder.loginGraph(navController: NavController) {
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
}
通过将该方法设为 NavGraphBuilder
上的扩展方法,您可以将其与预构建的 navigation
、composable
和 dialog
扩展方法一起使用:
NavHost(navController, startDestination = "home") {
...
loginGraph(navController)
...
}
与底部导航栏集成
通过在可组合项层次结构中的更高层级定义 NavController
,您可以将 Navigation 与其他组件(例如底部导航组件)相关联。这样,您就可以通过选择底部栏中的图标来进行导航。
如需使用 BottomNavigation
和 BottomNavigationItem
组件,请将 androidx.compose.material
依赖项添加到您的 Android 应用中。
Groovy
dependencies { implementation "androidx.compose.material:material:1.3.1" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.4.3" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.3.1") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.4.3" } 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
。然后,可通过 NavDestination
层次结构将该项的路由与当前目的地及其父目的地的路由进行比较来确定每个 BottomNavigationItem
的选定状态(以处理使用嵌套导航的情况)。
该项目的路由还用于将 onClick
lambda 连接到对 navigate
的调用,以便在点按该项时会转到该项。通过使用 saveState
和 restoreState
标志,当您在底部导航项之间切换时,系统会正确保存并恢复该项的状态和返回堆栈。
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
中检索到的参数的类型正确。
如需了解详情,请参阅 Navigation 类型安全文档。
互操作性
如果您想将 Navigation 组件与 Compose 配合使用,有以下两种选择:
- 使用基于 fragment 的 Navigation 组件定义导航图。
- 使用 Compose 目的地在 Compose 中通过
NavHost
定义导航图。只有在导航图中的所有界面都是可组合项的情况下,才可以这么做。
因此,若要构建 Compose 和 View 混合应用,我们建议使用基于 Fragment 的 Navigation 组件。然后,使用 Fragment 存储基于 View 的界面、Compose 界面和同时使用 View 和 Compose 的界面。每个 Fragment 的内容都在 Compose 中以后,下一步是将所有这些界面与 Navigation Compose 结合在一起,并移除所有 Fragment。
使用基于 fragment 的 Navigation 从 Compose 导航
要在 Compose 代码内更改目的地,您可以公开可传递到并由层次结构中的任何可组合项触发的事件:
@Composable
fun MyScreen(onNavigate: (Int) -> ()) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
在您的 fragment 中,您可以通过找到 NavController
并导航到目的地,在 Compose 和基于 fragment 的 Navigation 组件之间架起桥梁:
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
或者,您可以将 NavController
传递到 Compose 层次结构下方。不过,公开简单的函数的可重用性和可测试性更高。
测试
建议您将 Navigation 代码与可组合项目的地分离开,以便独立于 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
实例的实例传递给该对象。为此,Navigation 测试工件提供了一个 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
检查您的断言,只需使用 navController
的 currentBackStackEntry
将当前 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。
如需了解如何设计应用导航,使其适应不同的屏幕尺寸、屏幕方向和设备外形规格,请参阅自适应界面的导航。
如需了解模块化应用中的更高级 Navigation Compose 实现(包括嵌套图和底部导航栏集成等概念),请查看 Now in Android 代码库。