前言
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()
来返回到上一个屏幕。
路由跳转
跳转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
返回前页面传值
1 | navController.previousBackStackEntry?.savedStateHandle?.set("picPath", it) |
返回后的页面接收值
1 | // 获取当前页面的状态句柄 |
方式2
跳转前的页面
1 | navController.currentBackStackEntry?.savedStateHandle |
返回页面、返回回调参数
1 | val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle |
注意要在页面销毁的时候移除观察者,否则回调会触发多次。
页面返回重建
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 |