1. 项目概述
Jetpack Compose作为现代Android开发的声明式UI框架,其导航系统与传统Fragment导航有着本质区别。这个项目提供了一个完整的Compose Navigation实现方案,特别聚焦于交互动画的集成和功能完整性。我在实际项目中发现,很多团队在迁移到Compose时,最头疼的就是如何实现流畅的导航动画和保持完整的后退栈管理。
这套示例代码完整演示了以下核心场景:
- 基础页面跳转与参数传递
- 嵌套导航图的组织架构
- 自定义转场动画的精细控制
- 深层链接(DeepLink)的处理
- 底部导航栏与导航图的联动
2. 核心架构设计
2.1 导航图定义
采用单Activity架构,所有页面都是可组合函数。导航图的定义建议放在独立的NavGraph.kt文件中:
kotlin复制const val ROUTE_HOME = "home"
const val ROUTE_DETAIL = "detail/{id}"
fun MainNavGraph(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = ROUTE_HOME
) {
composable(ROUTE_HOME) {
HomeScreen(onItemClick = { id ->
navController.navigate("detail/$id")
})
}
composable(
route = ROUTE_DETAIL,
arguments = listOf(navArgument("id") { type = NavType.IntType })
) { backStackEntry ->
val id = backStackEntry.arguments?.getInt("id") ?: 0
DetailScreen(id = id)
}
}
}
关键点:路由常量应集中管理,避免硬编码。参数化路由使用URI风格的路径参数。
2.2 动画集成方案
Compose Navigation原生支持通过EnterTransition和ExitTransition定义转场动画。本项目的创新点在于实现了可组合的动画工厂:
kotlin复制class SlideAnimationSpec(
private val initialOffset: (fullWidth: Int) -> Int,
private val targetOffset: (fullWidth: Int) -> Int
) : AnimatedContentTransitionScope<Intent>.() -> ContentTransform {
override fun invoke(scope: AnimatedContentTransitionScope<Intent>): ContentTransform {
val width = scope.transition.layoutInfo.width
return slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Left,
initialOffset = { initialOffset(width) }
) togetherWith slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Right,
targetOffset = { targetOffset(width) }
)
}
}
// 使用示例
composable(
route = "profile",
enterTransition = { SlideAnimationSpec({ -it }, { 0 }).invoke(this) },
exitTransition = { SlideAnimationSpec({ 0 }, { it }).invoke(this) }
) { ProfileScreen() }
这种设计允许:
- 动画逻辑的复用
- 动态计算偏移量
- 支持自定义缓动曲线
- 与系统返回手势的自然配合
3. 完整功能实现
3.1 底部导航集成
底部导航栏需要与导航状态保持同步。关键实现要点:
kotlin复制@Composable
fun BottomBar(navController: NavHostController) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
BottomNavigation {
items.forEach { screen ->
BottomNavigationItem(
selected = currentRoute == screen.route,
onClick = {
navController.navigate(screen.route) {
// 避免重复点击时创建多个实例
launchSingleTop = true
// 恢复正确的返回栈状态
restoreState = true
}
},
icon = { Icon(Icons.Filled.Home, contentDescription = null) }
)
}
}
}
3.2 深层链接处理
支持从通知栏等场景直接跳转到特定页面:
kotlin复制composable(
route = "message/{id}",
deepLinks = listOf(
navDeepLink { uriPattern = "myapp://message/{id}" }
)
) { backStackEntry ->
val id = backStackEntry.arguments?.getString("id")
MessageScreen(id)
}
在AndroidManifest.xml中配置对应的intent-filter:
xml复制<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="message" />
</intent-filter>
</activity>
4. 高级技巧与避坑指南
4.1 返回栈管理陷阱
常见问题:多次快速点击导致重复导航
解决方案:使用launchSingleTop + restoreState
kotlin复制navController.navigate(route) {
launchSingleTop = true
restoreState = true
}
4.2 动画性能优化
发现卡顿时的检查清单:
- 确保动画使用
remember缓存动画规格 - 避免在转场过程中加载数据
- 对复杂内容使用
Crossfade替代位移动画 - 测试低端设备的动画表现
4.3 嵌套导航图的正确用法
对于模块化项目,建议按功能划分嵌套导航图:
kotlin复制fun AppNavGraph(navController: NavHostController) {
NavHost(navController, "main") {
navigation(startDestination = "home", route = "main") {
composable("home") { HomeScreen() }
composable("search") { SearchScreen() }
}
navigation(startDestination = "profile", route = "user") {
composable("profile") { ProfileScreen() }
composable("settings") { SettingsScreen() }
}
}
}
5. 测试方案
5.1 导航测试
使用TestNavHostController进行单元测试:
kotlin复制@Test
fun navigation_to_detail_screen() {
val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
navController.setGraph(R.navigation.main_graph)
onNodeWithText("Go to Detail").performClick()
assertThat(navController.currentBackStackEntry?.destination?.route)
.isEqualTo("detail")
}
5.2 动画测试
使用ComposeTestRule捕获帧时间:
kotlin复制@Test
fun slide_animation_duration() {
composeTestRule.setContent {
MyAppTheme {
MainScreen()
}
}
composeTestRule.onNodeWithText("Animate").performClick()
composeTestRule.waitForIdle()
val frameTimes = composeTestRule.onRoot().getAnimationFrameTimes()
assertThat(frameTimes.max() - frameTimes.min()).isLessThan(300L)
}
6. 完整示例结构
项目目录结构建议:
code复制/src/main/java/com/example/navigationdemo/
├── navigation/
│ ├── NavGraph.kt
│ └── Routes.kt
├── screens/
│ ├── HomeScreen.kt
│ ├── DetailScreen.kt
│ └── components/
├── MainActivity.kt
└── theme/
├── AnimationSpecs.kt
└── TransitionExtensions.kt
在实现过程中发现,将动画规格与屏幕内容分离可以显著提高代码可维护性。例如所有共享元素过渡可以集中定义在TransitionExtensions.kt中。