简单的页面跳转
在 Compose 中,我们可以借助 State 实现一个非常简单的屏幕内容切换效果。
kotlin
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ComposeNavigationTestTheme {
MyApp()
}
}
}
}
@Composable
fun MyApp(modifier: Modifier = Modifier) {
// 当前显示的页面
var currentScreen by remember { mutableIntStateOf(0) }
when (currentScreen) {
0 -> ScreenA(modifier = modifier, onClick = {
currentScreen = 1
})
1 -> ScreenB(modifier = modifier, onClick = {
currentScreen = 2
})
2 -> ScreenC(modifier = modifier, onClick = {
currentScreen = 0
})
}
}
@Composable
fun ScreenA(modifier: Modifier = Modifier, onClick: () -> Unit) {
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("这里是页面 A")
Button(onClick = onClick) {
Text("跳转到页面 B")
}
}
}
@Composable
fun ScreenB(modifier: Modifier = Modifier, onClick: () -> Unit) {
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("这里是页面 B")
Button(onClick = onClick) {
Text("跳转到页面 C")
}
}
}
@Composable
fun ScreenC(modifier: Modifier = Modifier, onClick: () -> Unit) {
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("这里是页面 C")
Button(onClick = onClick) {
Text("跳转到页面 A")
}
}
}
运行效果:

但当前的效果只有页面切换,没有页面导航。按下返回键时,只会回到桌面,无法回到上一个页面。
当然,你可以自己来实现导航功能,但我们更多会使用 Navigation 组件。
添加依赖
在 build.gradle.kts 文件中添加依赖:
kotlin
dependencies {
implementation("androidx.navigation:navigation-compose:2.9.6")
}
简单用法
修改 MyApp 函数:
kotlin
const val SCREEN_A = "screen_a"
const val SCREEN_B = "screen_b"
const val SCREEN_C = "screen_c"
@Composable
fun MyApp(modifier: Modifier = Modifier) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = SCREEN_A, modifier = modifier) {
// 路由字符串映射到Composable页面
composable(route = SCREEN_A) {
ScreenA {
// 跳转
navController.navigate(route = SCREEN_B)
}
}
composable(route = SCREEN_B) {
ScreenB {
navController.navigate(route = SCREEN_C)
}
}
composable(route = SCREEN_C) {
ScreenC {
navController.navigate(route = SCREEN_A)
}
}
}
}
我们定义了每个页面的路由,设置起始路由为 SCREEN_A,也就是启动页为 ScreenA。
这样就具备导航功能了,运行效果:

当前所在页面
要知道当前所在页面,可以使用 NavDestination 获取当前页面的路由。
kotlin
val navController = rememberNavController()
navController.currentDestination?.route
在 Compose 中,我们更多会使用 NavController.currentBackStackEntryAsState(),因为它返回的是一个 State。
kotlin
val navController = rememberNavController()
val currentBackStack by navController.currentBackStackEntryAsState()
currentBackStack?.destination?.route
启动模式
跳转的默认行为对应的是 Activity 中的 Standard 启动模式。
我们还可以在跳转时指定额外的行为。
popUpTo
kotlin
// 假设在 ScreenC 中跳转到 ScreenA
navController.navigate(route = SCREEN_A) {
popUpTo(route = SCREEN_A) {
inclusive = true
}
}
上面的代码表示在跳转到 ScreenA 之前,会不断让页面出栈,直到遇到传入 popUpTo() 函数的路由参数对应的页面(ScreenA),将 inclusive(包括)属性设为 true,那么在遇到目标页面时,也会将目标页面弹出栈。
这个组合常用于模拟 Activity 的 singleTask 启动模式的栈行为。
比如当前的返回栈是 A -> B -> C,如果从 C 跳回 A,我们希望清除掉 A 上面的 B 和 C,并且不希望有两个 A(销毁旧的 A),就可以使用它。
但请注意,这和 Activity 的
singleTask行为并不完全一致,具体可以看我的这篇博客:精通 Activity 四大启动模式。
launchSingleTop
launchSingleTop 属性的效果类似于 Activity 的 singleTop 启动模式。
kotlin
composable(route = SCREEN_A) {
ScreenA {
navController.navigate(route = SCREEN_A) {
launchSingleTop = true
}
}
}
当指定 launchSingleTop 为 true 时,NavController 会判断返回栈的顶部是否为导航目标页。如果是,NavController 就不会创建新的实例,而是会复用栈顶的实例。
避免了在栈顶多次创建相同的页面实例。
跳转时传递参数
必选参数
首先,修改 ScreenB,让它能够接收字符串参数 userId。
kotlin
@Composable
fun ScreenB(modifier: Modifier = Modifier, userId: String? = null, onClick: () -> Unit = {}) {
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("这里是页面 B")
Text(text = "用户id为:$userId")
Button(onClick = onClick) {
Text("跳转到页面 C")
}
}
}
接着修改 NavHost,使得路由节点能够支持参数传递。
kotlin
composable(
// 新增userId路由参数
route = "${SCREEN_B}/{userId}",
arguments = listOf(
// 指定路由参数的类型
navArgument(name = "userId") {
type = NavType.StringType
}
)
) { navBackStackEntry ->
// 获取参数数据
val userId = navBackStackEntry.arguments?.getString("userId")
ScreenB(userId = userId) {
navController.navigate(route = SCREEN_C)
}
}
最后,在页面跳转时,将参数传递进来。
kotlin
composable(route = SCREEN_A) {
ScreenA {
val userId = "123"
navController.navigate(route = "${SCREEN_B}/$userId")
}
}
运行效果:

可选参数
Navigation 也支持可选参数,类似于 URL 的查询参数。
修改 NavHost 中 ScreenB 的路由定义:
kotlin
composable(
// 定义可选参数
route = "${SCREEN_B}?userId={userId}",
arguments = listOf(
navArgument(name = "userId") {
type = NavType.StringType
nullable = true // 必须要允许为空
// defaultValue = "0" // 可提供默认值
}
)
) { navBackStackEntry ->
val userId = navBackStackEntry.arguments?.getString("userId") // 可能为 null
ScreenB(userId = userId) {
navController.navigate(route = SCREEN_C)
}
}
跳转时,可以携带参数,也可以不传入:
kotlin
composable(route = SCREEN_A) {
ScreenA {
// 带参数跳转
navController.navigate(route = "${SCREEN_B}?userId=456")
// 不带参数跳转
navController.navigate(route = SCREEN_B)
}
}
页面间返回结果
从 A 跳转到 B,如果还需要在 B 页面返回一个结果给 A,我们可以使用 SavedStateHandle 来完成
修改 ScreenB,使其能够返回结果:
kotlin
@Composable
fun ScreenB(modifier: Modifier = Modifier, onClick: (String) -> Unit = {}) {
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("这里是页面 B")
Button(onClick = { onClick("这是B返回的结果") }) {
Text("返回结果")
}
}
}
修改 NavHost 中 ScreenB 的路由定义:
kotlin
composable(
route = SCREEN_B
) { navBackStackEntry ->
ScreenB { result ->
// 将结果设置到上一个页面的SavedStateHandle中
navController.previousBackStackEntry
?.savedStateHandle
?.set("result_key", result)
// 回到上一个页面
navController.popBackStack() // 将当前页面弹出栈
}
}
现在,就能够在 ScreenA 中接收结果了:
kotlin
composable(route = SCREEN_A) { navBackStackEntry ->
// 这样也能获取到当前的栈节点
// navController.currentBackStackEntry
// 监听栈节点的参数
val result = navBackStackEntry.savedStateHandle.getStateFlow<String?>(
key = "result_key",
initialValue = null
).collectAsState()
ScreenA(resultFromB = result.value) {
// 移除参数
navBackStackEntry.savedStateHandle.remove<String>("result_key")
navController.navigate(route = SCREEN_B)
}
}
@Composable
fun ScreenA(modifier: Modifier = Modifier, resultFromB: String?, onClick: () -> Unit) {
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("这里是页面 A")
resultFromB?.let {
Text("从B返回的结果: $it")
}
Button(onClick = onClick) {
Text("跳转到页面 B")
}
}
}
高级导航技巧
嵌套导航图 (Nested Navigation)
当应用变得复杂时,我们可以嵌套导航图来让路由更加清晰。
kotlin
const val SCREEN_A = "screen_a"
const val SCREEN_B = "screen_b"
const val AUTH_FLOW_ROUTE = "auth_flow"
const val LOGIN_ROUTE = "login"
const val REGISTER_ROUTE = "register"
@Composable
fun MyComplexApp(modifier: Modifier = Modifier) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = SCREEN_A) {
// 主应用页面
composable(route = SCREEN_A) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("A")
Button(onClick = {
// 将跳转到子图的起始页 (LOGIN_ROUTE)
navController.navigate(route = AUTH_FLOW_ROUTE)
}) {
Text("跳转到登录页")
}
}
}
composable(route = SCREEN_B) {
Text("B")
}
// 子导航图-认证流程
navigation(
startDestination = LOGIN_ROUTE, // 子图的起始页
route = AUTH_FLOW_ROUTE // 子图的路由名
) {
composable(route = LOGIN_ROUTE) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("Login")
}
}
composable(route = REGISTER_ROUTE) {
Text("Register")
}
}
}
}
导航完成时,回退栈将会是 [ SCREEN_A, LOGIN_ROUTE ]。
ViewModel 作用域
默认情况下,ViewModel 的生命周期绑定到了 NavBackStackEntry 返回栈节点。
如果我们希望多个页面(Composable)共享一个 ViewModel,我们可以提升 ViewModel 的作用域到这些屏幕共同的嵌套导航图。
比如我们希望登录和注册页面能够共享一个邮箱数据,我们可以这样做:
kotlin
class SharedAuthViewModel : ViewModel() {
// 邮箱
private val _email = MutableStateFlow("")
val email: StateFlow<String> = _email
fun updateEmail(newEmail: String) {
_email.value = newEmail
}
init {
Log.d("AuthViewModel", "ViewModel created!")
}
override fun onCleared() {
super.onCleared()
Log.d("AuthViewModel", "ViewModel destroyed!")
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeNavigationTestTheme {
MyApp()
}
}
}
}
const val HOME_ROUTE = "home"
const val AUTH_FLOW_ROUTE = "auth_flow" // 嵌套图的路由
const val LOGIN_ROUTE = "login"
const val REGISTER_ROUTE = "register"
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = HOME_ROUTE) {
composable(HOME_ROUTE) {
HomeScreen {
navController.navigate(AUTH_FLOW_ROUTE)
}
}
// 认证流程子图
navigation(
startDestination = LOGIN_ROUTE,
route = AUTH_FLOW_ROUTE
) {
composable(LOGIN_ROUTE) { navBackStackEntry ->
// 获取子图的 NavBackStackEntry
val authGraphEntry = remember(navBackStackEntry) {
val parentRoute = navBackStackEntry.destination.parent!!.route!!
navController.getBackStackEntry(parentRoute)
}
// 将 ViewModel 作用域限定到这个子图
val authViewModel: SharedAuthViewModel = viewModel(authGraphEntry)
LoginScreen(
viewModel = authViewModel,
toRegister = { navController.navigate(REGISTER_ROUTE) },
toHome = {
// 回到主页
navController.popBackStack(HOME_ROUTE, inclusive = false)
})
}
composable(REGISTER_ROUTE) { navBackStackEntry ->
val authGraphEntry = remember(navBackStackEntry) {
val parentRoute = navBackStackEntry.destination.parent!!.route!!
navController.getBackStackEntry(parentRoute)
}
// 与 LoginScreen 获取到完全相同的 ViewModel 实例
val authViewModel: SharedAuthViewModel = viewModel(authGraphEntry)
RegisterScreen(viewModel = authViewModel) {
// 回到登录页
navController.navigate(LOGIN_ROUTE) {
popUpTo(route = LOGIN_ROUTE) {
inclusive = true
}
}
// 或者直接使用 navController.popBackStack()
}
}
}
}
}
@Composable
fun HomeScreen(onClick: () -> Unit = {}) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("主屏幕")
Button(onClick = onClick) {
Text("进入登录/注册流程")
}
}
}
@Composable
fun LoginScreen(
viewModel: SharedAuthViewModel,
toRegister: () -> Unit,
toHome: () -> Unit
) {
val email by viewModel.email.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("登录页")
TextField(
value = email,
onValueChange = { viewModel.updateEmail(it) },
label = { Text("共享的 Email") }
)
Spacer(Modifier.height(16.dp))
Button(onClick = toRegister) {
Text("跳转到注册页")
}
Spacer(Modifier.height(8.dp))
Button(onClick = toHome) {
Text("返回主页")
}
}
}
@Composable
fun RegisterScreen(viewModel: SharedAuthViewModel, toLogin: () -> Unit) {
val email by viewModel.email.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("注册页")
TextField(
value = email,
onValueChange = { viewModel.updateEmail(it) },
label = { Text("共享的 Email (与登录页同步)") }
)
Spacer(Modifier.height(16.dp))
Button(onClick = toLogin) {
Text("返回登录页")
}
}
}
popBackStack(route, inclusive) 的行为是不断从栈顶弹出页面,直到找到函数参数 route,如果 inclusive 标志为 true,那么 route 也会一并弹出。
底部导航栏的状态保存与恢复
在实现底部导航栏时,切换 Tab 往往会导致前一个 Tab 的状态(如滚动位置)丢失。
为了实现状态的保存与恢复,我们可以使用三个关键选项:
kotlin
// 当点击底部 Tab 时,调用此方法
fun NavController.onBottomNavClicked(route: String) {
this.navigate(route) {
// 弹出到图的起始点,避免回退栈无限累积
popUpTo(this@onBottomNavClicked.graph.findStartDestination().id) {
saveState = true // 关键:弹出时保存栈中页面的状态
}
// 避免在已位于栈顶时重复创建
launchSingleTop = true
// 关键:导航到目的地时,恢复其之前保存的状态
restoreState = true
}
}
这样能实现状态不丢失,并且在 首页 -> 搜索 -> 我的 来回切换时,回退栈只会是这几个:首页、首页 -> 搜索、首页 -> 我的。
示例代码:
kotlin
class TabViewModel(private val tabName: String) : ViewModel() {
private val _count = MutableStateFlow(0)
val count = _count.asStateFlow()
fun increment() {
_count.value++
}
init {
Log.d("ViewModelLifecycle", "$tabName ViewModel CREATED")
}
override fun onCleared() {
Log.d("ViewModelLifecycle", "$tabName ViewModel CLEARED")
super.onCleared()
}
}
// 底部 Tab 的路由
sealed class Screen(val route: String, val icon: ImageVector, val label: String) {
data object Home : Screen("home", Icons.Filled.Home, "首页")
data object Search : Screen("search", Icons.Filled.Search, "搜索")
data object Profile : Screen("profile", Icons.Filled.Person, "我的")
}
val bottomNavItems = listOf(
Screen.Home,
Screen.Search,
Screen.Profile
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeNavigationTestTheme {
MainScreen()
}
}
}
}
@Composable
fun MainScreen() {
val navController = rememberNavController()
Scaffold(
bottomBar = {
MyBottomNavBar(navController = navController)
}
) { paddingValues ->
NavHost(
navController = navController,
startDestination = Screen.Home.route,
modifier = Modifier.padding(paddingValues)
) {
composable(Screen.Home.route) {
// 手动创建 factory 来传递 tabName,以便观察日志
val vm: TabViewModel = viewModel(factory = TabViewModelFactory("Home"))
TabScreenContent(vm)
}
composable(Screen.Search.route) {
val vm: TabViewModel = viewModel(factory = TabViewModelFactory("Search"))
TabScreenContent(vm)
}
composable(Screen.Profile.route) {
val vm: TabViewModel = viewModel(factory = TabViewModelFactory("Profile"))
TabScreenContent(vm)
}
}
}
}
class TabViewModelFactory(private val tabName: String) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(TabViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return TabViewModel(tabName) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
// 底部导航栏
@Composable
fun MyBottomNavBar(navController: NavController) {
NavigationBar {
// 当前路由状态
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
bottomNavItems.forEach { screen ->
NavigationBarItem(
selected = currentRoute == screen.route,
onClick = {
navController.navigate(screen.route) {
// 弹出到图的起始点,在这里相当于是:
// popUpTo(Screen.Home.route) {
// saveState = true
// inclusive = false
// }
popUpTo(navController.graph.findStartDestination().id) {
// 关键:弹出屏幕时,保存它们的状态
saveState = true
}
// 避免在已位于栈顶时重复创建实例
launchSingleTop = true
// 关键:导航到目的地时,恢复其之前保存的状态
restoreState = true
}
},
icon = { Icon(screen.icon, contentDescription = screen.label) },
label = { Text(screen.label) }
)
}
}
}
// Tab 屏幕内容
@Composable
fun TabScreenContent(viewModel: TabViewModel) {
val count by viewModel.count.collectAsState()
val listState = rememberLazyListState() // 用于演示滚动位置的保存
Column(modifier = Modifier.fillMaxSize()) {
Text(text = "当前计数值: $count", style = MaterialTheme.typography.headlineMedium)
Button(onClick = { viewModel.increment() }) {
Text("点我 +1")
}
LazyColumn(state = listState) {
items(100) {
Text(text = "我是列表项 $it", modifier = Modifier.padding(16.dp))
}
}
}
}
在这里起始页是 home,假设当前的返回栈是 [home, profile],点击 search 标签页时将会发生:
-
popUpTo(navController.graph.findStartDestination().id):将除了起始页(home)的页面都弹出栈,返回栈会变为[home]。(inclusive默认为false) -
saveState = true:将弹出的页面的ViewModel和 UI 状态进行保存。 -
launchSingleTop = true:防止重复点击同一个 Tab 时,创建多个页面实例。 -
restoreState = true:导航到search页时,会先看看之前有没有为search页保存过状态。- 如果有,会直接唤醒之前的
search实例,恢复其ViewModel和 UI 状态(这里是滚动位置)。 - 如果没有,意味着是第一次点击,会创建一个全新的
search页面实例。
- 如果有,会直接唤醒之前的
支持 deep Link
deep link 就是可以让第三方(如浏览器、其他应用等)直接唤起指定的页面。
首先,让当前的 Activity 支持 deep link,修改 AndroidManifest.xml 文件:
xml
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.ComposeNavigationTest">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</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" />
<!-- 支持 myapp://screen_b 格式-->
<data
android:host="screen_b"
android:scheme="myapp" />
</intent-filter>
</activity>
然后在 ScreenB 页面的路由定义处加上 deep link 的支持即可。
kotlin
composable(
route = "${SCREEN_B}/{userId}",
arguments = listOf(
navArgument(name = "userId") {
type = NavType.StringType
}
),
// 添加 deepLinks 列表
deepLinks = listOf(
navDeepLink {
// 定义 URI 格式,它会自动匹配路由参数
uriPattern = "myapp://$SCREEN_B/{userId}"
}
)
) { navBackStackEntry ->
val userId = navBackStackEntry.arguments?.getString("userId")
ScreenB(userId = userId) {
navController.navigate(route = SCREEN_C)
}
}
只要进入 "myapp://screen_b/321" 链接,即可在打开页面 B 的同时传递 userId 为 "321" 的参数。
参考
参考郭霖大佬的博客:写给初学者的Jetpack Compose教程,Navigation