前言
Navigation Compose 是 Jetpack Compose 中的一个导航库,它可以帮助你在基于 Compose 的 Android 应用中实现屏幕之间的导航。
这个就类似Vue中的vue-router,可以使用也可以不使用。
以下是使用 Navigation Compose 的详细步骤:
添加依赖
在你的项目的 build.gradle 文件中添加 Navigation Compose 的依赖:1
2
3
4
5dependencies {
implementation("androidx.navigation:navigation-compose:2.7.7")
// 动画扩展
implementation("com.google.accompanist:accompanist-navigation-animation:0.32.0")
}
路由导航
AppNavigation.kt
定义导航图
导航图是一个描述应用中所有可导航目的地的图。
可以使用 NavGraphBuilder 来定义导航图。
基本示例
以下是一个简单的示例:
1 | import androidx.navigation.NavGraphBuilder |
在这个示例中,我们定义了一个名为 root 的导航图,它的起始目的地是 screenLogin。
导航图中有两个可导航的目的地:screenLogin 和 screenMain。
使用路由常量
1 | // 定义路由常量(推荐使用密封类) |
创建导航宿主
导航宿主是一个 Composable,它负责显示当前的目的地。你可以使用 NavHost 来创建导航宿主。
基本示例
以下是一个示例:
1 | import androidx.compose.runtime.Composable |
在这个示例中,我们使用 rememberNavController 来创建一个 NavController,它负责管理导航状态。
然后,我们使用 NavHost 来创建导航宿主,并将 rootNavGraph 作为导航图传递给它。
跳转动画
1 |
|
主Composable使用导航
最后,我们需要在主 Composable 中使用 AppNavigation:
1 | import androidx.compose.foundation.layout.fillMaxSize |
Activity设置主Composable
这样项目就需要一个Activity了。
在你的 Activity 中,使用 setContent 方法来设置 MyApp 作为内容:
1 | import androidx.appcompat.app.AppCompatActivity |
通过以上步骤,你就可以在你的 Jetpack Compose 应用中使用 Navigation Compose 来实现屏幕之间的导航了。
创建屏幕Composable
现在,我们需要创建每个屏幕的 Composable。
以下是 ScreenLogin 和 ScreenMain 的示例:
ScreenLogin
1 | import androidx.compose.foundation.layout.Arrangement |
ScreenMain
1 | import androidx.compose.foundation.layout.Arrangement |
在 ScreenLogin 中,我们使用 navController.navigate("screenMain") 来导航到 ScreenMain。
在 ScreenMain 中,我们使用 navController.popBackStack() 来返回到上一个屏幕。
路由地址监听
路由模板
1 | val navController = rememberNavController() |
查看路由地址带参数
1 | DisposableEffect(Unit) { |
路由跳转
跳转Push
1 | navController.navigate("screenMain") |
跳转防重复
1 | fun NavController.navigateSingle(route: String, popUpToRoute: String? = null) { |
使用
1 | // 使用方式 |
替换路由Replace
直接调用
1 | navController.navigate("screenMain") { |
关键原理说明
popUpTo(currentRoute):指定要弹出到哪个路由inclusive = true:表示将currentRoute本身也从返回栈中移除- 这样新导航的页面会替代被弹出的页面,用户返回时会直接回到更早的页面
返回
1 | navController.popBackStack() |
返回到指定路由
1 | navController.popBackStack(route = MyScreen.ScreenMain.route, inclusive = false) |
函数扩展
1 | import androidx.navigation.NavController |
调用
1 | navController.replace("screenReport") |
清空历史跳转登录
1 | navController.navigate(MyScreen.ScreenLogin.route) { |
关键在于 navigate 中的配置参数
通过以下两点实现 “清空历史”:
launchSingleTop
launchSingleTop = true
避免重复创建登录页实例(若登录页已在栈顶,则直接复用,防止栈内出现多个登录页)。
popUpToRoute
在 Navigation Compose 中,popUpToRoute(通常通过 popUpTo() 方法配置)的核心作用是管理导航栈中的页面回退行为,允许你在导航到新目的地时,将栈中某些已存在的页面弹出,从而控制导航历史的结构。
具体来说,它的主要用途包括:
清理导航栈
当你导航到新页面时,通过 popUpToRoute 可以指定一个目标路由,系统会将导航栈中该路由之上的所有页面全部弹出。
例如:从 A → B → C 导航到 D 时,如果设置 popUpTo("A"),栈会变成 A → D,中间的 B、C 被移除。
配合 inclusive 参数使用
inclusive = false(默认):仅弹出指定路由之上的页面,保留指定路由本身。inclusive = true:弹出指定路由及其之上的所有页面(包括指定路由本身)。
路由传参
参数空字符串
传参不能是空字符串
传参不能是空字符串
传参不能是空字符串
空的话路由会报错。
所以一定要判断参数。
如果要传空字符串,我们可以传类似于”0”,组件中再判断。
例如
1 | composable( |
单个参数
定义带参数的路径
创建一个生成实际路由的方法,替换占位符
1 | sealed class MyScreen { |
传递的参数不能是对象。
定义接收参数
在 NavHost 中定义可组合的屏幕,包括带参数的屏幕
1 | // 详情屏幕(带参数) |
导航到带参数的屏幕
使用 createRoute 方法生成实际的路由字符串。
1 | navController.navigate(MyScreen.Detail.createRoute("123")) |
多个参数
定义带参数的路径
创建一个生成实际路由的方法,替换占位符
1 | sealed class MyScreen { |
传递的参数不能是对象。
定义接收参数
在 NavHost 中定义可组合的屏幕,包括带参数的屏幕
1 | composable( |
屏幕
1 |
|
导航到带参数的屏幕
使用 createRoute 方法生成实际的路由字符串。
1 | navController.navigate(MyScreen.ScreenVideoPlay.createRoute("123",1)) |
参数中带/
如果参数中带有/,路由跳转会报错。
处理方案:
- 传递参数时进行编码:使用
URLEncoder对包含特殊字符的参数进行编码 - 接收参数时进行解码:使用
URLDecoder对参数进行解码还原
编码
1 | val encStr = URLEncoder.encode(imgUrl, "UTF-8") |
解码
1 | val decStr = URLDecoder.decode(imgUrl, "UTF-8") |
参数编码
1 | sealed class MyScreen(val route: String) { |
路由组件中解码
1 | composable( |
起始路由传参
使用navController.navigate(MyScreen.ScreenSubjectQues.createRoute("1956278715385204738"))才可以。
正确写法
1 |
|
错误写法
在startDestination中设置,虽然能跳转到指定页面,但是获取不到参数。
1 | navigation( |
页面返回传值
返回前页面传值
1 | navController.previousBackStackEntry?.savedStateHandle?.set("picPath", it) |
返回后的页面接收值
1 | // 获取当前页面的状态句柄 |
页面返回重建
A页面跳转到B页面,再返回到A页面,A页面会触发重建。
Navigation Compose 在返回时会重新创建上一个页面的 Composable(这是官方设计的默认行为)。
这时候组件内的LaunchedEffect也会重新执行
1 | LaunchedEffect(Unit) { |
这和我们平时开发逻辑就不太一样。
我们第一想到就是怎样让返回不重建,但是这是错误的。
Navigation Compose 的设计理念就是通过重组更新 UI,强制阻止重建可能会导致状态不一致、内存泄漏等问题。
保留页面实例会增加内存消耗,尤其是在复杂页面或多页面场景下。
可以的方案是
使用
rememberSaveable或 ViewModel 保存状态(即使页面重建也能恢复)是更合理的选择,既符合 Compose 范式,又能达到类似 “不重建” 的用户体验。
添加初始化的状态
1 | class BaseViewModel : ViewModel() { |
页面中
1 | LaunchedEffect(Unit) { |
这样把状态保留在ViewModel中,即使重建也不影响页面。
页面可见状态监听
1 | DisposableEffect(navController.currentBackStackEntry?.lifecycle) { |
返回确认
1 | // 控制弹窗显示状态 |
显示退出弹窗
1 | // 3. 确认弹窗:点击「退出」则返回上一页,「取消」则关闭弹窗 |
原来按钮事件中的
1 | navController.popBackStack() |
要改成
1 | showQuitDialog = true |
按键返回监听失效
方式1(无效)
如果使用 Navigation Compose,当 NavHost 内部有可返回的路由栈(即非根页面)时,默认会优先执行路由的 popBackStack,会导致全局 BackHandler 逻辑被跳过。
1 | BackHandler { |
如果多个地方都写了 BackHandler,返回键触发时不会所有回调都执行,而是遵循 “优先级规则”:只有最内层且 enabled = true 的 BackHandler 会触发,外层的不会执行。
核心规则:
内层优先,且仅触发一个
方式2(无效)
下面这种全局监听返回的方式,也会在路由能返回的时候失效,和方式1效果一样。
因为路由会接管返回的监听。
1 | class MainActivity : ComponentActivity() { |
全局注册
1 |
|
方式3(有效)
还是监听路由进行回调分发才有效
1 |
|
注意当不能返回的时候再调用
1 | navController.popBackStack() |
会导致以后返回失效