原文: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

1.引入依赖

implementation "androidx.navigation:navigation-compose:2.5.3"

2.声明NavController

首先,就是使用rememberNavController获得NavController对象

//导航
val navController = rememberNavController()

PS: 之后的跳转和返回上一级就是使用此对象过来就行

3.使用NavHost,声明页面路由

在布局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 = "设置页面")
}
}

3.页面跳转

下面则是两个重要的方法:

  • navController.navigate() 页面跳转
  • navController.popBackStack() 回上一级,相当于点击返回键

这里,我们结合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,则有下面的步骤:

1.修改NavHost定义路由

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等方法也要修改哦!

可选参数

上面的例子,实际上是必传的一个参数,如果需要可选参数,有以下规则:

  1. 可选参数必须使用查询参数语法?argName={argName} 来添加
  2. 可选参数必须具有 defaultValue nullable = true (将默认值设置为 null)

第二点就是上面提到过的两个属性,例如我们加多一个age的可选参数:

composable("SettingPage/{id}?age={age}", arguments = listOf(
navArgument("id") {
type = NavType.IntType
}, navArgument("age") {
type = NavType.IntType
defaultValue = 0
}
)) {
//省略...
}

2.调用传参

上面说了那么多,那么应该如何去调用呢?

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.currentBackStackEntryAsState()navBackStackEntry?.destination

您可以利用 NavController.currentBackStackEntryAsState() 方法从 NavHost 函数中获取 navController 状态,并与 BottomNavigation 组件共享此状态。这意味着 BottomNavigation 会自动拥有最新状态。

不过官方使用的是密封类里构造底部菜单数据,我这里则是使用了list直接构造,也差不了太多

@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 = "设置")
}
}

深层链接

深层链接实际就是Intent-Filter的用法,这里还是比较少用,就不准备详细说明了,具体使用可以参考导航 - Jetpack Compose Docs

参考

Jetpack Compose学习(11)——Navigation页面导航的使用的更多相关文章

  1. Jetpack Compose学习(7)——MD样式架构组件Scaffold及导航底部菜单

    Jetpack Compose学习(7)--MD样式架构组件Scaffold及导航底部菜单 | Stars-One的杂货小窝 Compose给我们提供了一个Material Design样式的首页组件 ...

  2. Jetpack Compose学习(1)——从登录页开始入门

    原文地址:Jetpack Compose学习(1)--从登录页开始入门 | Stars-One的杂货小窝 Jetpack Compose UI在前几天出了1.0正式版,之前一直还在观望,终于是出了正式 ...

  3. Jetpack Compose学习(3)——图标(Icon) 按钮(Button) 输入框(TextField) 的使用

    原文地址: Jetpack Compose学习(3)--图标(Icon) 按钮(Button) 输入框(TextField) 的使用 | Stars-One的杂货小窝 本篇分别对常用的组件:图标(Ic ...

  4. Jetpack Compose学习(2)——文本(Text)的使用

    原文: Jetpack Compose学习(2)--文本(Text)的使用 | Stars-One的杂货小窝 对于开发来说,文字最为基础的组件,我们先从这两个使用开始吧 本篇涉及到Kotlin和DSL ...

  5. Jetpack Compose学习(5)——从登录页美化开始学习布局组件使用

    原文:Jetpack Compose学习(5)--从登录页美化开始学习布局组件使用 | Stars-One的杂货小窝 本篇主要讲解常用的布局,会与原生Android的布局控件进行对比说明,请确保了解A ...

  6. Jetpack Compose学习(9)——Compose中的列表控件(LazyRow和LazyColumn)

    原文:Jetpack Compose学习(9)--Compose中的列表控件(LazyRow和LazyColumn) - Stars-One的杂货小窝 经过前面的学习,大致上已掌握了compose的基 ...

  7. Jetpack Compose学习(4)——Image(图片)使用及Coil图片异步加载库使用

    原文地址 Jetpack Compose学习(4)--Image(图片)使用及Coil图片异步加载库使用 | Stars-One的杂货小窝 本篇讲解下关于Image的使用及使用Coil开源库异步加载网 ...

  8. Jetpack Compose学习(6)——关于Modifier的妙用

    原文: Jetpack Compose学习(6)--关于Modifier的妙用 | Stars-One的杂货小窝 之前学习记录中也是陆陆续续地将常用的Modifier的方法穿插进去了,本期就来详细的讲 ...

  9. Jetpack Compose学习(8)——State及remeber

    原文地址: Jetpack Compose学习(8)--State状态及remeber关键字 - Stars-One的杂货小窝 之前我们使用TextField,使用到了两个关键字remember和mu ...

  10. WinPhone学习笔记(一)——页面导航与页面相关

    最近学一下Windows Phone(接下来简称“WinPhone”)的开发,在很久很久前稍探究一下WinPhone中对一些传感器的开发,那么现在就从头来学学WinPhone的开发.先从WinPhon ...

随机推荐

  1. cs231n__2. K-nearest Neighbors

    CS231n 2 K-Nearest Neighbors note ---by Orangestar 1. codes: import numpy as np class NearestNeighbo ...

  2. 1+x初级Web的关键词填写

    H5+CSS: 声明HTML网页标准:<!DOCTYPE> 图片标签 img css颜色样式color 定位 position 绝对absolute 相对 relative 外边距:mar ...

  3. APICloud 入门教程窗口篇

    什么是窗口,窗口可以理解为一屏幕内容的一个基本载体,里面可以放导航,图片,视频,文字等组成一屏幕内容. 不同的窗口组成一个APP, 例如购物APP有[首页],[购物车],[我的]等不同的窗口.不同的窗 ...

  4. Android applink 踩坑指南

    Android applink 踩坑指南 原理 接入步骤 将链接与activity关联起来 加入meta data 生成身份验证JSON 真机测试 结论 官方文档 原理 与url scheme不同的地 ...

  5. 内网渗透-at&schtasks&impacket的使用

    内网机器结构 机器账号密码如下: 2008 r2 webserver 域内 web 服务器 本地管理员账号密码 : .\administraotr:admin!@#45 当前机器域用户密码 : god ...

  6. day07-Vue04

    Vue04 12.Vue2 脚手架模块化开发 目前开发模式的问题: 开发效率低 不够规范 维护和升级,可读性比较差 12.1基本介绍 官网地址 什么是Vue Cli脚手架 12.2环境配置,搭建项目 ...

  7. iOS开发小结 - 通过PUT请求上传数据

    一般服务器上传数据一般都是用POST请求,这样通过AFNetworking的POST请求稳稳的,但是有一天遇到一个问题,服务器上传数据用的是PUT请求,发现用AFNetworking并不是那么好用,今 ...

  8. Linux 驱动像单片机一样读取一帧dmx512串口数据

    硬件全志R528 目标:实现Linux 读取一帧dmx512串口数据. 问题分析:因为串口数据量太大,帧与帧之间的间隔太小.通过Linux自带的读取函数方法无法获取到 帧头和帧尾,读取到的数据都是缓存 ...

  9. 使用英特尔 Sapphire Rapids 加速 PyTorch Transformers 模型

    大约一年以前,我们 展示 了如何在第三代 英特尔至强可扩展 CPU (即 Ice Lake) 集群上分布式训练 Hugging Face transformers 模型.最近,英特尔发布了代号为 Sa ...

  10. 02安装一个最小化的Hadoop

    安装一个最小化的Hadoop 为了学习HDFS和之后的MapReduce,我们需要安装一个Hadoop. Hadoop一共有3种运行模式 独立模式:不启动守护进程,所有程序运行在一个JVM进程中.独立 ...