Navigation 组件支持 Jetpack Compose 应用。您可以在利用 Navigation 组件的基础架构和功能的同时,在可组合项之间进行导航。
设置
如需支持 Compose,请在应用模块的 build.gradle
文件中使用以下依赖项:
dependencies { def nav_version = "2.8.8" implementation "androidx.navigation:navigation-compose:$nav_version" }
dependencies { val nav_version = "2.8.8" 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)
}
借助这些深层链接,您可以将特定网址、操作或 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/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
,您可以将 Navigation 与其他组件(例如底部导航组件)相关联。这样,您就可以通过选择底部栏中的图标来进行导航。
如需使用 BottomNavigation
和 BottomNavigationItem
组件,请将 androidx.compose.material
依赖项添加到您的 Android 应用中。
dependencies { implementation "androidx.compose.material:material:1.7.5" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
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
。然后,可通过 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()
方法从 NavHost
函数中获取 navController
状态,并与 BottomNavigation
组件共享此状态。这意味着 BottomNavigation
会自动拥有最新状态。
互操作性
如果您想将 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) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
在您的 fragment 中,您可以通过找到 NavController
并导航到目的地,在 Compose 和基于 fragment 的 Navigation 组件之间架起桥梁:
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
或者,您可以将 NavController
传递到 Compose 层次结构下方。不过,公开简单的函数的可重用性和可测试性更高。
测试
将导航代码与可组合项目的地分离,以便独立于 NavHost
可组合项单独测试每个可组合项。
这意味着,您不应直接将 navController
传入任何可组合项,而应以参数形式传递导航回调。这样一来,您的所有可组合项均可单独测试,因为它们不需要在测试中使用 navController
实例。
借助 composable
lambda 提供的间接层,您可以将 Navigation 代码与可组合项本身分离开。这在以下两个方向上可行:
- 仅将解析后的参数传递到可组合项
- 传递应由要导航的可组合项触发的 lambda,而不是
NavController
本身。
例如,如果某个 ProfileScreen
可组合项接受 userId
作为输入,并允许用户导航到好友的个人资料页面,则可以采用以下签名:
@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
的界面测试如下所示:
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()
}
}
测试导航操作
您可以通过多种方式测试导航实现,具体方法是:点击界面元素,然后验证显示的目的地;或者比较预期路线与当前路线。
如果您要测试具体应用的实现,最好在界面中执行点击操作。如需了解如何单独测试此组件以及各个可组合函数,请务必查看 Testing in 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,请参阅 Navigation 组件使用入门或完成 Jetpack Compose Navigation Codelab。
如需了解如何设计应用导航,使其适应不同的屏幕尺寸、屏幕方向和设备外形规格,请参阅自适应界面的导航。
如需了解模块化应用中的更高级 Compose 导航栏实现(包括嵌套图和底部导航栏集成等概念),请查看 GitHub 上的 Now in Android 应用。
示例
您可以在一条文本字符串中支持多个链接,以便用户选择要前往的位置并提高互动度。 您可以设置文本的部分样式,以提高可读性、提升用户体验,并通过使用颜色和字体来激发更大的创造力。 您可以在用户在文本字段中输入内容(例如输入姓名、电子邮件地址、地址或其他联系信息)时验证输入内容。此验证可减少错误并为用户节省时间。 如需在应用中显示图片(用于内容和响应用户操作),请从磁盘或互联网上的外部来源加载图片。 卡片可为界面提供 Material Design 容器。 使用 TopAppBar 可组合项创建顶部应用栏,以帮助用户在应用中导航和访问功能。 了解如何使用 Compose Animation API 为状态值添加动画效果、使用转场效果添加动画效果、为可见性或大小更改添加动画效果,以及添加交叉淡化效果。 应用栏是位于屏幕顶部或底部的容器,其中包含主要功能和导航项。 您可以创建一个图标,以便根据用户的切换开关隐藏或显示密码,从而提高安全性并提升用户体验。 您可以加载可绘制文件来显示动画图片,从而在应用中打造更具互动性和吸引力的用户体验。动画图片非常适合用于创建加载指示器、成功或错误指示器,以及促进游戏开发和各种其他界面功能。 限制应用在小屏设备上的屏幕方向,但不限制其在大屏设备上的屏幕方向。 您可以在图片的剪裁区域周围绘制阴影,以便以剪辑的形状显示图片。 了解如何管理可拆卸键盘配置更改。 进度指示器会显示操作的状态。 创建分页列表,以便用户滚动浏览无法在一屏中显示的内容。水平分页列表可帮助用户浏览图片、幻灯片或商品轮播界面等内容。垂直分页列表非常适合内容丰富的应用,在这些应用中,用户可能需要滚动浏览大量内容(例如文章)。 您可以使用切换开关让用户选择两种状态之一。 为界面选择合适的组件,并了解如何在应用中实现该组件。 按钮可触发特定操作。 条状标签组件可直观地表示复杂的实体,通常包含图标和标签。 构建您的首个 Jetpack Compose 测试。了解如何使用 Compose 的测试工件编写界面测试、使用测试规则、查找器和断言。 用户可以通过悬浮操作按钮在应用中执行主要操作。 拒绝触控笔手掌误触。 对话框会在应用主要内容之上的层上显示弹出式消息或请求用户输入。 框架可将界面的不同部分(例如应用栏和悬浮操作按钮)整合在一起,从而让应用具有一致的外观和风格。支持在单个文本字符串中添加多个链接
设置文本的部分样式
在用户输入时验证输入内容
加载和显示图片
创建一个用作容器的卡片
显示顶部应用栏
Compose 中的动画
显示应用栏
根据用户切换开关显示或隐藏密码
显示动画图片
限制应用在手机上的屏幕方向,但不限制其在大屏设备上的屏幕方向
显示裁剪为某个形状的图片
管理可拆卸键盘配置变更
创建进度指示器
显示分页列表
添加用户可以切换的开关
显示互动组件
创建按钮
创建用于表示复杂实体的条状标签
在 Compose 中进行测试
创建悬浮操作按钮 (FAB)
拒绝触控笔手掌轻触
显示弹出式消息或请求用户输入
创建一个框架组件来将界面整合在一起
目前没有任何推荐文档页面。
请尝试登录您的 Google 账号。