使用 Compose 进行导航
Navigation 组件支持 Jetpack Compose 应用,允许在利用 Navigation 组件基础架构和功能的同时,在可组合项之间进行导航。
添加依赖项
在应用模块的 build.gradle 文件中添加以下依赖项:
dependencies {
def nav_version = "2.8.9"
implementation "androidx.navigation:navigation-compose:$nav_version"
}
Kotlin DSL 版本:
dependencies {
val nav_version = "2.8.9"
implementation("androidx.navigation:navigation-compose:$nav_version")
}
基础概念
实现导航时,需要实现三个核心组件:导航宿主、导航图和导航控制器。与 View 系统不同,Compose 使用 NavHost 作为导航宿主,使用可组合函数定义导航图,使用 NavController 进行导航操作。
创建导航控制器
使用 rememberNavController() 创建并记住 NavController 实例:
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(navController, startDestination = "profile") {
// 导航图
}
}
设计导航图
使用 NavHost 组件和 composable 函数构建导航图。每条路线定义一个可组合项,当导航到该路线时会显示:
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(navController, startDestination = "profile") {
composable("profile") { ProfileScreen() }
composable("friendslist") { FriendsListScreen() }
}
}
带参数的导航
可以使用类型安全的参数定义路线:
@Serializable
data class Profile(val id: String)
@Serializable
data class FriendsList(val type: String)
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(navController, startDestination = Profile("user123")) {
composable<Profile> { backStackEntry ->
// 获取传递的参数
val profile = backStackEntry.toRoute<Profile>()
ProfileScreen(profile.id)
}
composable<FriendsList> { backStackEntry ->
val friendsList = backStackEntry.toRoute<FriendsList>()
FriendsListScreen(friendsList.type)
}
}
}
导航到可组合项
使用 NavController 的 navigate 方法在目的地之间导航:
navController.navigate("profile")
// 类型安全的导航
navController.navigate(Profile("username"))
在目的地之间传递数据
强烈建议不要在导航时传递复杂数据对象,而是传递最少必要信息(如唯一标识符),然后在目标屏幕中从数据层加载完整数据:
// 仅传递用户ID
navController.navigate(Profile(id = "user1234"))
在目标 ViewModel 中,使用 SavedStateHandle 检索参数:
class UserViewModel(
savedStateHandle: SavedStateHandle,
private val userInfoRepository: UserInfoRepository
) : ViewModel() {
private val profile = savedStateHandle.toRoute<Profile>()
// 从数据层获取用户信息
private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(profile.id)
// ...
}
深层链接
Navigation Compose 支持深层链接,可定义为 composable() 函数的一部分:
@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)
}
要向外部应用公开深层链接,需在 manifest.xml 中添加 intent-filter:
<activity ...>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
与底部导航集成
将 NavController 与底部导航关联,可通过选择底部栏中的图标进行导航:
dependencies {
implementation "androidx.compose.material:material:1.7.8"
}
Kotlin DSL 版本:
dependencies {
implementation("androidx.compose.material:material:1.7.8")
}
实现底部导航:
data class TopLevelRoute<T : Any>(val name: String, val route: T, val icon: ImageVector)
val topLevelRoutes = listOf(
TopLevelRoute("Profile", Profile, Icons.Profile),
TopLevelRoute("Friends", Friends, Icons.Friends)
)
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) {
// 避免返回堆栈过大
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// 避免同一目的地的多个副本
launchSingleTop = true
// 重新选择时恢复状态
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(navController, startDestination = Profile, Modifier.padding(innerPadding)) {
composable<Profile> { ProfileScreen(...) }
composable<Friends> { FriendsScreen(...) }
}
}
将 Navigation Compose 与 fragment 集成
有两种选择将 Navigation 与 Compose 配合使用:
- 使用基于 fragment 的 Navigation 组件定义导航图
- 在 Compose 中通过 NavHost 定义导航图
对于混合应用,建议使用基于 Fragment 的 Navigation 组件,然后将每个 fragment 的内容迁移到 Compose。最后,将所有界面与 Navigation Compose 关联并移除所有 fragment。
在 fragment 中与 Compose 交互:
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest ->
findNavController().navigate(dest)
})
}
}
最佳实践
将导航代码与可组合项分离,以便独立测试:
@Composable
fun ProfileScreen(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) { ... }
// 导航图中
composable<Profile> { backStackEntry ->
val profile = backStackEntry.toRoute<Profile>()
ProfileScreen(userId = profile.id) { friendUserId ->
navController.navigate(route = Profile(id = friendUserId))
}
}
测试导航
添加导航测试依赖项:
dependencies {
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
}
封装 NavHost 以便测试:
@Composable
fun AppNavHost(navController: NavHostController){
NavHost(navController = navController){
// ...
}
}
编写测试:
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)
}
}
@Test
fun appNavHost_verifyStartDestination() {
composeTestRule
.onNodeWithContentDescription("Start Screen")
.assertIsDisplayed()
}
}