原文:Jetpack Compose学习(11)——Navigation页面导航的使用 - Stars-One的杂货小窝
在Android原生的View开发中的,也是有Navigation,原生我之后可能再出篇教程,今天讲解的则是compose版本的Navigation组件的使用
本系列以往文章请查看此分类链接Jetpack compose学习
在原生中,官方比较推荐使用单Activity+多Fragment模式来构建一个APP,而Fragment的切换需要使用FragmentManager来进行管理,比如add replace等方法,对于开发者来说,也是比较繁琐,于是官方在Jetpack组件中就是推出了Navigation的库,可以方便开发者快速去管理多Fragment的页面堆栈问题
而今天的主要针对compose,compose的架构也是一个Activity+多个可组合项,而如何去切换可组合项?
以往的做法就是定义一个选中下标,下标更改了,重新渲染一下页面,从而达到切换页面的效果
而有了Navigation,则是可以在使用层面更为优雅和规范,也是方便管理,不易出现奇奇怪怪的堆栈问题等
这里我们以一个使用了NavigationBar组件的页面进行改造
PS:这里我是新版本的AS创建的项目,默认使用的是Material3的依赖包,所以底部导航栏组件就是
NavigationBar,如果是Material2的依赖包,则是叫BottomNavigation
效果如下图所示:

就是实现了两个底部菜单,然后可以进行页面的切换,代码如下所示:
@OptIn(ExperimentalMaterial3Api::class)
@Preview(showBackground = true)
@Composable
fun MyApp() {
var currentSelect by remember {
mutableStateOf(0)
}
val menuData = listOf(
BottomItemData("首页", Icons.Filled.Home),
BottomItemData("设置", Icons.Filled.Settings)
)
Scaffold(modifier = Modifier.fillMaxSize()
, bottomBar = {
NavigationBar() {
menuData.forEachIndexed { index, bottomItemData ->
NavigationBarItem(
selected = index == currentSelect,
onClick = {
currentSelect = index
},
icon = {
Icon(
imageVector = bottomItemData.icon,
contentDescription = "点击按钮"
)
},
label = {
Text(
text = (bottomItemData.label)
)
},
)
}
}
}
) { innerPadding ->
//下面第3步就是对此部分代码进行更改
//IDE强制要使用者innerPadding,这里就简单的打印一下
println(innerPadding)
if (currentSelect == 0) {
Text("首页")
}else{
Text("设置")
}
}
}
从上面代码中,我们可以看到我们是通过currentSelect这个变量来进行页面的显示管理,实际上这里管理会比较繁琐,这个时候就得用到今天的主角Navigation了
implementation "androidx.navigation:navigation-compose:2.5.3"
首先,就是使用rememberNavController获得NavController对象
//导航
val navController = rememberNavController()
PS: 之后的跳转和返回上一级就是使用此对象过来就行
在布局content参数的函数里使用下面这段代码:
NavHost(navController = navController, startDestination = "MainPage") {
//声明名为MainPage的页面路由
composable("MainPage"){
//页面路由对应的页面组件
MainPage()
}
composable("SettingPage"){
SettingPage()
}
}
上面是声明了两个页面,MainPage和SettingPage则是它两的路由名,后面页面跳转需要使用到
NavHost方法中,参数需要上一步的NavController对象,startDestination则是开始的页面为MainPage
简单的解释,就是可以把NavHost也看出一个页面组件,这个组件默认显示的是MainPage页面
@OptIn(ExperimentalMaterial3Api::class)
@Preview(showBackground = true)
@Composable
fun MyApp() {
var currentSelect by remember {
mutableStateOf(0)
}
//导航
val navController = rememberNavController()
val menuData = listOf(
BottomItemData("首页", Icons.Filled.Home),
BottomItemData("设置", Icons.Filled.Settings)
)
Scaffold(modifier = Modifier.fillMaxSize()
, bottomBar = {
NavigationBar() {
menuData.forEachIndexed { index, bottomItemData ->
NavigationBarItem(
selected = index == currentSelect,
onClick = {
currentSelect = index
},
icon = {
Icon(
imageVector = bottomItemData.icon,
contentDescription = "点击按钮"
)
},
label = {
Text(
text = (bottomItemData.label)
)
},
)
}
}
}
) { innerPadding ->
//IDE强制要使用者innerPadding,这里就简单的打印一下
println(innerPadding)
NavHost(navController = navController, startDestination = "MainPage") {
composable("MainPage"){
MainPage()
}
composable("SettingPage"){
SettingPage()
}
}
}
}
@Preview(showBackground = true)
@Composable
fun MainPage(){
Column() {
Text(text = "主页")
}
}
@Preview(showBackground = true)
@Composable
fun SettingPage() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "设置页面")
}
}
下面则是两个重要的方法:
这里,我们结合NavigationBar实现点击底部按钮切换不同的页面
实现效果如下图所示:

代码如下图所示

PS:这里需要注意,如果使用了
navController.navigate()来进行页面跳转,实际上就是和打开一个新的Activity类似,存在一个页面堆栈比如说上图的效果,如果点返回键,则是回到上次的页面,而不会立即退出APP
而且,每次调用
navigate()会创建一个新的页面对象
如果需要单一创建一个对象,可以这样写
navController.navigate("MainPage"){
launchSingleTop = true
}
之后不过怎么跳转,都会用的同一个页面对象
不过一般我们会是在页面之间跳转的,比如说要再MainPage页面跳转到SettingPage页面,要怎么写呢?

向MainPage传递NavController的参数,之后页面中的按钮来可以使用此对象来完成页面跳转的功能
不过官方比较推荐我们传递一个函数参数,如下代码所示:
//方法需要传函数参数
@Composable
fun MainPage(toSettingPage:()->Unit) {
Column() {
Text(text = "主页")
Button(onClick = { toSettingPage.invoke() }) {
Text("跳转到设置页面")
}
}
}
NavHost(navController = navController, startDestination = "MainPage") {
composable("MainPage"){
//这里也要同步修改
//下面的是kotlin中的lambda的简化写法,不明白的可以先去了解下
MainPage(){
navController.navigate("SettingPage")
}
}
composable("SettingPage"){
SettingPage()
}
}
上面的基本使用,例子也是比较简单
一般情况下,我们可能跳转到新页面的时候需要传递数据,比如说从列表页到详情页,需要传递点击的item项的id,从而详情页可以拿到id去请求接口从而获得详情的数据并展示,这个时候该如何操作呢?
针对上面的代码,我们假设进入Setting页面,需要传递一个字符串的参数名为id,则有下面的步骤:
NavHost(navController = navController, startDestination = "MainPage") {
composable("MainPage"){
MainPage(){
navController.navigate("SettingPage")
}
}
composable("SettingPage/{id}"){
//这里获取传参过来数据
val id = it.arguments?.getString("id")
//可以传入SettingPage中,然后在页面中使用数据,这里我就没写了
SettingPage()
}
}
这里上面,可以看到加了个{id}(有点类似Spring Boot里的接口写法),就是标明需要传参的意思
这里实际上还可以声明类型,默认不写的话,就是说id是String类型的
arguments这个变量实际上就是我们Intent中常用的Bundle,所以获取数值的是调用了getString()方法
如果想要id是整数的话,应该怎么做呢?
如下面代码所示:
composable("SettingPage/{id}", arguments = listOf(navArgument("id") {
type = NavType.IntType
})) {
//省略....
}
通过arguments参数来设置传参对应配置,接收的是一个列表,也很好理解,因为会有多个参数嘛
而list里的元素的类型,则是NamedNavArgument,官方提供navArgument()方法方便我们快速构造NamedNavArgument类型
我们可以看看navArgument方法的参数
navArgument(
name: String,
builder: NavArgumentBuilder.() -> Unit
)
name则是需要与我们路由上定义的参数名一致,而builder则是一个函数,我们可以写个函数来快速进行参数的配置,主要可以设置以下三个属性:
type 类型defaultValue 默认值nullable 是否可空PS:改了参数类型,对应的取值调用的getXX等方法也要修改哦!
上面的例子,实际上是必传的一个参数,如果需要可选参数,有以下规则:
?argName={argName} 来添加defaultValue 或 nullable = true (将默认值设置为 null)第二点就是上面提到过的两个属性,例如我们加多一个age的可选参数:
composable("SettingPage/{id}?age={age}", arguments = listOf(
navArgument("id") {
type = NavType.IntType
}, navArgument("age") {
type = NavType.IntType
defaultValue = 0
}
)) {
//省略...
}
上面说了那么多,那么应该如何去调用呢?
NavHost(navController = navController, startDestination = "MainPage") {
composable("MainPage") {
MainPage() {
//跳转的时候传参
navController.navigate("SettingPage/11?age=18")
}
}
composable("SettingPage/{id}?age={age}", arguments = listOf(
navArgument("id") {
type = NavType.IntType
defaultValue = 0
nullable = true
}, navArgument("age") {
type = NavType.IntType
defaultValue = 0
}
)) {
//这里获取传参过来数据
val id = it.arguments?.getInt("id")
Log.d("starsone", "MyApp: $id")
SettingPage()
}
}
由于上面,我们是将跳转封装在了函数,所以只需要调整MainPage路由里的函数即可navController.navigate("SettingPage/11?age=18")
当然,这里age是可以不传的,默认则是0
在上面的例子的,我们涉及到的页面也是比较少,但是,在APP更新迭代之后,页面可以会成倍地增加
这个时候我们如果还是每个路由直接写死的一个字符串变量,那么万一哪天不小心改了,就会导致路由无法跳转了,针对这个问题,解决思路也比较简单,使用枚举或者常量来管理路由名就可
object Pages{
const val MainPage = "mainPage"
const val SettingPage = "settingPage"
}
NavHost(navController = navController, startDestination = Pages.MainPage) {
composable(Pages.MainPage) {
MainPage() {
navController.navigate("${Pages.SettingPage}/11")
}
}
composable("${Pages.SettingPage}/{id}?age={age}", arguments = listOf(
navArgument("id") {
type = NavType.IntType
defaultValue = 0
nullable = true
}, navArgument("age") {
type = NavType.IntType
defaultValue = 0
}
)) {
//这里获取传参过来数据
val id = it.arguments?.getString("id")
Log.d("starsone", "MyApp: $id")
SettingPage()
}
}
除此之外,如果NavHost里的composable多了起来,也不好方便管理是吧,我们可以按照业务逻辑来进行一个嵌套导航(嵌套导航结构)
我自己按照理解称呼为多路由组合,即把页面包含下钻页面的几个页面整合到一起
在NavHost里使用navigation()来实现嵌套导航结构:
NavHost(navController, startDestination = "home") {
...
// 当导航到 login 路由时,自动找到 username 目标进行显示
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
...
}
为了更好的复用,我们还可以将其抽取成一个方法(这里官方就是使用了扩展方法):
fun NavGraphBuilder.loginGraph(navController: NavController) {
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
}
NavHost(navController, startDestination = "home") {
...
// 当导航到 login 路由时,自动找到 username 目标进行显示
loginGraph()
...
}
上面也是说到官方不推荐我们传NavController实例到composable里,因为composable需要单元测试,且单元测试的时候传个NavController太多余
所以推荐我们使用函数
但是这里研究过后,发现有个问题官方没有提到,如果我页面里可能有几个按钮,且会跳转到不同的参数呢?
这个时候,就是需要对函数进行封装,我们以上面给的代码为例
@Composable
fun MainPage(toSettingPage:()->Unit) {
Column() {
Text(text = "主页")
Button(onClick = { toSettingPage.invoke() }) {
Text("跳转到设置页面")
}
}
}
NavHost(navController = navController, startDestination = "MainPage") {
composable("MainPage"){
//这里也要同步修改
//下面的是kotlin中的lambda的简化写法,不明白的可以先去了解下
MainPage(){
navController.navigate("SettingPage")
}
}
composable("SettingPage"){
SettingPage()
}
}
下面进行修改
NavHost(navController = navController, startDestination = Pages.PAGE_MAIN) {
//这里定义一个页面跳转的action,接收路由和参数列表数据
val navAction = { route: String, args: List<Pair<String, Any>> ->
val value = args.joinToString("&") { it.first + "=" + it.second }
val fullRoute = when {
args.isEmpty() -> route
route.contains("?") -> "{$route}$value"
else -> "$route?$value"
}
//拼接路由和参数列表,形成真正的路由
Log.d("starsone", "MyApp跳转路径: $fullRoute")
//调用方法进行页面跳转
navController.navigate(fullRoute) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
composable("subject") {
SubjectPage(navController = navController)
}
composable(Pages.PAGE_MAIN) {
MainPage(navController = navController)
}
composable(Pages.PAGE_SETTING) {
SettingPage()
}
}
@Composable
fun IndexPage(lambda: (String, List<Pair<String, Any>>) -> Unit) {
Surface() {
//传递路由和名称
Button(onClick = { lambda.invoke("setting", listOf(Pair("age",10))) }) {
Text("测试")
}
}
}
上面是我想到的比较好的解决方案了,如果子页面也需要页面跳转,可以将函数进行往下传递
实际上,我个人觉得还是有些不优雅,因为还是去修改需要页面跳转的组件的参数列表,且因为多了个参数,导致组件就没法预览了,有些麻烦
不过上面是我想到的比较好的解决方案了,如果各位有更好的方案,欢迎评论区提出
这里以官方给出的示例,贴下代码并讲解
使用到了navController.currentBackStackEntryAsState()和navBackStackEntry?.destination
您可以利用 NavController.currentBackStackEntryAsState() 方法从 NavHost 函数中获取 navController 状态,并与 BottomNavigation 组件共享此状态。这意味着 BottomNavigation 会自动拥有最新状态。
不过官方使用的是密封类里构造底部菜单数据,我这里则是使用了list直接构造,也差不了太多
可以看到只要在首页的话,再次点击即可退出APP,不会出现多余的页面堆栈的情况,动图如下:

@Composable
fun MyApp() {
var currentSelect by remember {
mutableStateOf(0)
}
//导航
val navController = rememberNavController()
val menuData = listOf(
BottomItemData(Pages.PAGE_MAIN,"首页", Icons.Filled.Home),
BottomItemData(Pages.PAGE_SETTING,"设置", Icons.Filled.Settings)
)
Scaffold(modifier = Modifier.fillMaxSize()
, bottomBar = {
NavigationBar() {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
menuData.forEach { bottomItemData ->
NavigationBarItem(
//设置当前项是否为选中
selected = currentDestination?.hierarchy?.any { it.route == bottomItemData.route } == true,
icon = {
Icon(
imageVector = bottomItemData.icon,
contentDescription = "点击按钮"
)
},
label = {
Text(
text = (bottomItemData.label)
)
},
onClick = {
navController.navigate(bottomItemData.route) {
//使用此方法,可以避免生成一个重复的路由堆栈
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
//避免重复选择会创建一个新的页面副本
launchSingleTop = true
// Restore state when reselecting a previously selected item
//当重新选择之前已选择项目恢复页面状态
restoreState = true
}
},
)
}
}
}
) { innerPadding ->
println(innerPadding)
NavHost(navController = navController, startDestination = Pages.PAGE_MAIN) {
composable(Pages.PAGE_MAIN) {
MainPage()
}
composable(Pages.PAGE_SETTING) {
SettingPage()
}
}
}
}
//页面的枚举类
object Pages{
const val PAGE_MAIN = "main"
const val PAGE_SETTING = "setting"
}
//底部菜单的实体数据类,包含路由,文字和图标
data class BottomItemData(val route:String,val label: String, val icon: ImageVector)
//两个页面
@Composable
fun MainPage() {
Surface() {
Text(text = "首页")
}
}
@Composable
fun SettingPage() {
Surface() {
Text(text = "设置")
}
}
这里,我们可能会遇到一种常见的情况,在首页有个按钮,点击之后会进入到一个新的页面,而新的页面里是没有底部导航栏的,这种情况要怎么实现呢?
开始我也没啥头绪,直到看到了上面官方的例子,我们可以学以致用,还是用currentDestination这个对象来实现,我们只需要在bottomBar渲染出NavigationBar进行判断就可以了,如下面的代码所示:
//为了代码可读性,就稍微省略掉了一些代码
val menuData = listOf(
BottomItemData(Pages.PAGE_MAIN, "首页", Icons.Filled.Home),
BottomItemData(Pages.PAGE_SETTING, "设置", Icons.Filled.Settings)
)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
Scaffold(modifier = Modifier.fillMaxSize(), bottomBar = {
//只要当页面为首页或者为设置页面,才展示底部菜单栏
if (currentDestination?.hierarchy?.any { it.route == Pages.PAGE_MAIN || it.route == Pages.PAGE_SETTING } == true) {
NavigationBar() {...}
}
}
当然,我们还可以再进一步优化下写法:
val menuData = listOf(
BottomItemData(Pages.PAGE_MAIN, "首页", Icons.Filled.Home),
BottomItemData(Pages.PAGE_SETTING, "设置", Icons.Filled.Settings)
)
//将底部栏菜单数据的路由名整成一个list
val routes = menuData.map { it.route }
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
Scaffold(modifier = Modifier.fillMaxSize(), bottomBar = {
//只要当页面为首页或者为设置页面,才展示底部菜单栏
if (currentDestination?.hierarchy?.any { routes.contains(it.route) } == true) {
NavigationBar() {...}
}
}
PS:如果想要隐藏右侧的悬浮按钮,也是同理
之后下图就是演示的效果(实际上比较简单了,按钮跳转到另外的页面就可以了,代码就不贴出来了)

上面的示例一般情况下可以正常使用,但是如果你的页面需要传参,就会导致底部栏无法出现选中的情况
对于此情况,需要将判断进行修改即可,如以下代码(其他杂的代码就省略了..)
NavigationBar() {
menuData.forEach { bottomItemData ->
NavigationBarItem(
//设置当前项是否为选中
selected = currentDestination.hierarchy.any { it.route?.contains( bottomItemData.route) == true}
)
}
}
上面代码,简单讲解下,如果某个页面需要传参,路由名为setting?age={age},那么我们的bottomItemData.rote即为setting
所以才需要contains来进行判断即可满足条件,一般来说不会出现相同的路由名,所以使用上稍微注意下既可以了
深层链接实际就是Intent-Filter的用法,这里还是比较少用,就不准备详细说明了,具体使用可以参考导航 - Jetpack Compose Docs
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po