Jetpack Compose Navigation —— 页面跳转与返回栈管理

本文深入讲解 Compose Navigation 的全部用法,从基础跳转到深层链接、BottomNavigation 联动、动画转场。配合 Compose 从 0 到 1 阅读效果最佳。

Navigation 三大核心组件

Compose Navigation 由三个核心组件构成:

组件作用创建方式
NavController管理返回栈,控制跳转rememberNavController()
NavHost路由表,把路由和界面绑定NavHost(navController, startDestination) { }
composable()注册一个路由对应的页面composable("route") { Screen() }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Composable
fun AppNavigation() {
val navController = rememberNavController() // ① 创建 NavController

NavHost(
navController = navController,
startDestination = "home" // ② 默认首页
) {
composable("home") { // ③ 路由"home" → HomeScreen
HomeScreen(navController)
}
composable("detail") {
DetailScreen(navController)
}
}
}

⚠️ rememberNavController() 必须在 NavHost 外部创建,如果在 NavHost 内部创建会导致每次重组都重新生成。

路由定义

字符串路由(基础)

1
2
3
4
5
6
// 方式一:直接写字符串(适合简单场景)
composable("home") { HomeScreen() }
composable("profile") { ProfileScreen() }

// 跳转
navController.navigate("profile")

常量管理(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
object Routes {
const val HOME = "home"
const val DETAIL = "detail/{itemId}"
const val SETTINGS = "settings"
const val PROFILE = "profile/{userId}"

// 辅助函数:生成带参数的路由
fun detail(itemId: String) = "detail/$itemId"
fun profile(userId: Int) = "profile/$userId"
}

// 使用
navController.navigate(Routes.detail("abc123"))

✅ 集中管理路由常量,方便跳转和避免拼写错误。

路由参数速查表

参数写法含义示例路由
{arg}必选参数detail/{id}detail/123
{arg}={default}带默认值list/{page}=1
{arg}?argType=Int指定类型user/{id}arguments.add(NavType)
?arg={arg}可选查询参数search?q={query}

参数传递

必选参数(路径参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 路由定义
composable(
route = "detail/{itemId}",
arguments = listOf(
navArgument("itemId") { type = NavType.StringType }
)
) { backStackEntry ->
val itemId = backStackEntry.arguments?.getString("itemId")
DetailScreen(itemId = itemId ?: "")
}

// 跳转
navController.navigate("detail/abc123")

可选参数(查询参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 路由定义
composable(
route = "profile?userId={userId}&showEdit=false",
arguments = listOf(
navArgument("userId") {
type = NavType.IntType
defaultValue = -1 // ← 默认值
},
navArgument("showEdit") {
type = NavType.BoolType
defaultValue = false
}
)
) { backStackEntry ->
val userId = backStackEntry.arguments?.getInt("userId") ?: -1
val showEdit = backStackEntry.arguments?.getBoolean("showEdit") ?: false
ProfileScreen(userId, showEdit)
}

// 跳转(两种方式都可以)
navController.navigate("profile?userId=42")
navController.navigate("profile?userId=42&showEdit=true") // 全部参数

🔑 可选参数 = ? + = + defaultValue,三者缺一不可。

参数类型对照表

NavTypeKotlin 类型取值方法
StringTypeStringgetString("key")
IntTypeIntgetInt("key")
LongTypeLonggetLong("key")
FloatTypeFloatgetFloat("key")
BoolTypeBooleangetBoolean("key")
StringArrayTypeArray<String>getStringArray("key")
IntArrayTypeIntArraygetIntArray("key")

导航操作 —— 跳转、返回、清栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 基础跳转
navController.navigate("detail")

// navigate 常用选项
navController.navigate("detail") {
// 1. 启动模式:单例(返回栈已存在则复用)
launchSingleTop = true

// 2. 弹出页面(跳转前把当前页移出返回栈)
popUpTo(Routes.HOME) { inclusive = true } // 回退到 HOME,并移除 HOME 本身

// 3. 恢复之前的状态
restoreState = true
}

popBackStack() 返回

1
2
3
4
5
6
7
8
// 返回到上一个页面
navController.popBackStack()

// 返回到指定路由(不包含该路由本身)
navController.popBackStack("home", inclusive = false)

// 返回到指定路由(并移除该路由本身)
navController.popBackStack("home", inclusive = true)
1
2
3
4
5
// 等同于物理返回键
navController.navigateUp()

// 如果返回栈已是根节点,回到上一个 Activity
// 等同于 navController.popBackStack()

导航选项组合实战

1
2
3
4
5
// 场景:登录成功后跳转主页,且按返回键不再回到登录页
navController.navigate("home") {
popUpTo("login") { inclusive = true } // 把登录页也清掉
launchSingleTop = true // 防止重复创建主页
}

返回栈管理

返回栈原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NavHost 启动 startDestination="home"
┌─────────┐
│ home │ ← 根页面
└─────────┘

navigate("detail")
┌─────────┐
│ detail │ ← 推到栈顶
├─────────┤
│ home │
└─────────┘

popBackStack()
┌─────────┐
│ home │ ← 栈顶回到这里
└─────────┘

获取当前路由

1
2
3
4
5
6
7
8
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route

// 典型用途:高亮当前 tab
BottomNavigationItem(
selected = currentRoute == "home",
...
)

监听导航事件

1
2
3
4
5
6
7
// 监听路由变化(例如埋点统计)
val currentEntry by navController.currentBackStackEntryAsState()
LaunchedEffect(currentEntry) {
currentEntry?.destination?.route?.let { route ->
Log.d("Navigation", "当前页面:$route")
}
}

Deep Link —— 从外部打开指定页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
composable(
route = "detail/{itemId}",
arguments = listOf(
navArgument("itemId") { type = NavType.StringType }
),
deepLinks = listOf(
navDeepLink { uriPattern = "myapp://detail/{itemId}" },
navDeepLink {
uriPattern = "https://example.com/detail/{itemId}"
action = Intent.ACTION_VIEW
}
)
) { backStackEntry ->
val itemId = backStackEntry.arguments?.getString("itemId") ?: ""
DetailScreen(itemId)
}

AndroidManifest.xml 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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="detail" />
</intent-filter>
<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="https"
android:host="example.com"
android:pathPrefix="/detail" />
</intent-filter>
</activity>

BottomNavigation + Navigation 联动

标准实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
data class BottomNavItem(
val label: String,
val icon: ImageVector,
val route: String
)

@Composable
fun MainScreen() {
val navController = rememberNavController()

val items = listOf(
BottomNavItem("首页", Icons.Default.Home, "home"),
BottomNavItem("搜索", Icons.Default.Search, "search"),
BottomNavItem("消息", Icons.Default.Notifications, "message"),
BottomNavItem("我的", Icons.Default.Person, "profile"),
)

Scaffold(
bottomBar = {
NavigationBar {
val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route

items.forEach { item ->
NavigationBarItem(
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) },
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
// 防止多个栈的同一个页面重叠
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(
navController = navController,
startDestination = "home",
modifier = Modifier.padding(innerPadding)
) {
composable("home") { HomeScreen() }
composable("search") { SearchScreen() }
composable("message") { MessageScreen() }
composable("profile") { ProfileScreen() }
}
}
}

⚠️ 关键popUpTo(findStartDestination) + saveState + restoreState 这个三件套可防止切换 tab 时重复创建页面并保持滚动位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Scaffold(
// 侧边护栏(宽度 > 600dp 时自动推荐)
// 用法和 NavigationBar 完全一样
) { innerPadding ->
NavigationRail {
items.forEach { item ->
NavigationRailItem(
icon = { Icon(item.icon, null) },
label = { Text(item.label) },
selected = currentRoute == item.route,
onClick = { ... },
alwaysShowLabel = false
)
}
}
}

动画转场

composable 动画参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
composable(
route = "detail/{id}",
enterTransition = {
slideInHorizontally { it } + fadeIn() // 从右侧滑入 + 淡入
},
exitTransition = {
slideOutHorizontally { -it } + fadeOut() // 向左滑出 + 淡出
},
popEnterTransition = {
slideInHorizontally { -it } + fadeIn() // 返回时从左侧滑入
},
popExitTransition = {
slideOutHorizontally { it } + fadeOut() // 返回时向右滑出
}
) {
DetailScreen(it)
}

常用动画速查表

动画方法效果
slideInHorizontally { it }从右侧滑入
slideInHorizontally { -it }从左侧滑入
slideInVertically { it }从下方滑入
slideInVertically { -it }从上方滑入
fadeIn(initialAlpha = 0f)淡入
fadeOut(targetAlpha = 0f)淡出
scaleIn(initialScale = 0.8f)缩放进入
scaleOut(targetScale = 0.8f)缩放退出
slideInHorizontally { it } + fadeIn()组合动画(常见的推入+淡入)

🔧 it 代表完整宽度(水平)或完整高度(垂直),用 { it / 2 } 可控制滑动距离。

AnimatedNavHost 全局动画(Material 3)

1
2
3
4
5
6
7
8
9
10
11
// build.gradle.kts
implementation("androidx.compose.animation:animation:1.6.0")

@Composable
fun AppNavigation() {
val navController = rememberNavController()
AnimatedNavHost(navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("detail") { DetailScreen(navController) }
}
}

嵌套导航图

当模块较多时,可以把路由分组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NavHost(navController, startDestination = "main") {
// 嵌套导航图
navigation(
route = "auth",
startDestination = "login"
) {
composable("login") { LoginScreen() }
composable("register") { RegisterScreen() }
composable("forgotPassword") { ForgotPasswordScreen() }
}

// 主模块
navigation(
route = "main",
startDestination = "home"
) {
composable("home") { HomeScreen() }
composable("search") { SearchScreen() }
}

// 独立页面
composable("settings") { SettingsScreen() }
}

跳转到嵌套路由navController.navigate("auth/register")

常见坑与最佳实践

原因解决
快速双击跳转多个相同页面navigate 默认允许重复launchSingleTop = true
切换 tab 后页面状态丢失没有 saveState/restoreStateBottomNavigation 三件套
参数获取为空未声明 navArgument必须用 arguments = listOf(...) 声明
返回键直接退出应用返回栈为空判断 navController.previousBackStackEntry == null 时给提示
跳转后按返回又回到原始页没 popUpTo登录成功跳转后 popUpTo("login") { inclusive = true }
深层链接打不开AndroidManifest 没配 intent-filter两个 intent-filter 都要加

完整实战示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
object Routes {
const val HOME = "home"
const val DETAIL = "detail/{id}"
const val CART = "cart"
fun detail(id: String) = "detail/$id"
}

@Composable
fun ShopApp() {
val navController = rememberNavController()

NavHost(navController, startDestination = Routes.HOME) {
// 首页
composable(Routes.HOME) {
HomeScreen(
onItemClick = { id -> navController.navigate(Routes.detail(id)) },
onCartClick = { navController.navigate(Routes.CART) { launchSingleTop = true } }
)
}

// 详情页
composable(
route = Routes.DETAIL,
arguments = listOf(navArgument("id") { type = NavType.StringType }),
enterTransition = { slideInHorizontally { it } + fadeIn() },
exitTransition = { fadeOut() },
deepLinks = listOf(navDeepLink { uriPattern = "shop://detail/{id}" })
) { entry ->
DetailScreen(
id = entry.arguments?.getString("id") ?: "",
onBack = { navController.popBackStack() }
)
}

// 购物车
composable(Routes.CART) {
CartScreen(onBack = { navController.popBackStack() })
}
}
}