如果使用纯Compose开发Android应用,在页面导航方面navigation-compose几乎是唯一选择。
介绍一下navigation-compose的简单使用。
本篇文章Demo下载
依赖
scss
implementation("androidx.navigation:navigation-compose:2.7.5")
普通跳转
首先需要获取控制器NavController:
ini
val navController = rememberNavController()
NavController维护了Navigation 内部关于页面的堆栈、状态信息、导航图。
然后需要一个NavHost对象,定义导航的入口,同时也是承载导航页面的容器。
NavHost内部持有 NavController,在页面切换时渲染UI。通过 composable() 构建路线(节点)。
scss
@Composable
fun NavHostDemo() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = RouteConfig.ROUTE_PAGE_ONE) {
composable(RouteConfig.ROUTE_PAGE_ONE) {
PageOne(navController)
}
composable(RouteConfig.ROUTE_PAGE_TWO) {
PageTwo(navController)
}
composable(RouteConfig.ROUTE_PAGE_THREE) {
PageThree(navController)
}
}
}
上面将页面1设置为起始导航,并且设置了导航对应关系:
- RouteConfig.ROUTE_PAGE_ONE对应PageOne,
- RouteConfig.ROUTE_PAGE_TWO对应PageTwo,
- RouteConfig.ROUTE_PAGE_THREE对应PageThree,
由于页面都需要跳转,所以都传入NavController。
RouteConfig是用于方便管理路由地址和路由参数等的配置文件:
csharp
object RouteConfig {
/**
* 页面1路由
*/
const val ROUTE_PAGE_ONE = "pageOne"
/**
* 页面2路由
*/
const val ROUTE_PAGE_TWO = "pageTwo"
/**
* 页面3路由
*/
const val ROUTE_PAGE_THREE = "pageThree"
}
跳转
由于NavHost中设置了startDestination = RouteConfig.ROUTE_PAGE_ONE,而RouteConfig.ROUTE_PAGE_ONE对应PageOne,因此在应用打开时,PageOne会被加入到页面堆栈中,并跳转到PageOne。
使用navigate方法会将目的地添加到页面堆栈中,并跳转到目的地。
下面在PageOne中,我们设置点击按钮将触发navController.navigate(RouteConfig.ROUTE_PAGE_TWO),将RouteConfig.ROUTE_PAGE_TWO对应的PageTwo添加到页面堆栈中,并跳转到PageTwo,最终页面堆栈是PageOne、PageTwo。
ini
@Composable
fun PageOne(navController: NavController) {
Column(
modifier = Modifier
.fillMaxSize()
.background(
Color.White
)
) {
Text(text = "这是页面1")
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = {
//普通跳转
navController.navigate(RouteConfig.ROUTE_PAGE_TWO)
}) {
Text(
text = "跳转页面2",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
}
}
}
条件跳转
navigate方法在跳转时还可以附带条件,为了方便显示,我们再创建一个PageThree,在PageTwo中跳转到PageThree:
ini
@Composable
fun PageTwo(name: String?, age: Int, navController: NavController) {
Column(
modifier = Modifier
.fillMaxSize()
.background(
Color.White
)
) {
Text(text = "这是页面2")
Text(text = "name:$name,age:$age")
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = {
navController.navigate(RouteConfig.ROUTE_PAGE_THREE)
}) {
Text(
text = "跳转页面3",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
}
}
}
最终页面堆栈是PageOne、PageTwo、PageThree。
- popUpTo:传入一个目的地,在跳转新页面之前会将页面堆栈中直到目的地的所有可组合项弹出,然后跳转新页面。
scss
// 在进入RouteConfig.ROUTE_PAGE_THREE之前,回退栈会弹出所有的可组合项,直到 RouteConfig.ROUTE_PAGE_ONE
navController.navigate(RouteConfig.ROUTE_PAGE_THREE) {
popUpTo(RouteConfig.ROUTE_PAGE_ONE)
}
当前页面堆栈是PageOne、PageTwo,会弹出直到PageOne之前的所有可组合项,也就是会把PageTwo弹出,页面堆栈只剩下PageOne,然后将新页面PageThree加入页面堆栈中,并跳转PageThree。最终页面堆栈是PageOne、PageThree。
- inclusive:配合popUpTo使用,配置 inclusive = true 会将目的地也弹出。
javascript
// 在进入RouteConfig.ROUTE_PAGE_THREE之前,回退栈会弹出所有的可组合项,直到 RouteConfig.ROUTE_PAGE_ONE,并且包括它
navController.navigate(RouteConfig.ROUTE_PAGE_THREE) {
popUpTo(RouteConfig.ROUTE_PAGE_ONE) { inclusive = true }
}
当前页面堆栈是PageOne、PageTwo,会弹出直到PageOne之前的所有可组合项,并且包含PageOne,因此PageOne、PageTwo都被弹出,页面堆栈为空,然后将新页面PageThree加入页面堆栈中,并跳转PageThree。最终页面堆栈是只有PageThree。
- launchSingleTop:对应 Android 的 SingleTop,实现栈顶复用
javascript
// 对应 Android 的 SingleTop,如果回退栈顶部已经是 RouteConfig.ROUTE_PAGE_THREE,就不会重新创建
navController.navigate(RouteConfig.ROUTE_PAGE_THREE) {
launchSingleTop = true
}
返回
navigateUp方法可以返回到上一级页面,popBackStack可以返回到指定页面,如果不指定页面,就是将当前页面弹出,也就是返回上一级页面,同navigateUp。
ini
@Composable
fun PageThree(navController: NavController) {
Column(
modifier = Modifier
.fillMaxSize()
.background(
Color.White
)
) {
Text(text = "这是页面3")
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = {
navController.navigateUp() //返回上一级界面
// navController.popBackStack() //可以指定返回的界面(不指定就相当于navigateUp())。
}) {
Text(
text = "返回",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
}
}
}
携带参数跳转
使用配置文件方便管理参数名称:
csharp
object ParamsConfig {
/**
* 参数-name
*/
const val PARAMS_NAME = "name"
/**
* 参数-age
*/
const val PARAMS_AGE = "age"
}
必传参数
修改NavHost中PageTwo的路由,使之需要携带参数:
scss
//必传参数,使用"/"拼写在路由地址后面添加占位符
composable("${RouteConfig.ROUTE_PAGE_TWO}/{${ParamsConfig.PARAMS_NAME}}/{${ParamsConfig.PARAMS_AGE}}",
arguments = listOf(
navArgument(ParamsConfig.PARAMS_NAME) {},//参数是String类型可以不用额外指定
navArgument(ParamsConfig.PARAMS_AGE) {
type = NavType.IntType //指定具体类型
defaultValue = 25 //默认值(选配)
nullable = false //可否为null(选配)
}
)
) {
//通过composable函数中提供的NavBackStackEntry提取参数
val argument = requireNotNull(it.arguments)
val name = argument.getString(ParamsConfig.PARAMS_NAME)
val age = argument.getInt(ParamsConfig.PARAMS_AGE)
PageTwo(name, age, navController)
}
传递参数
直接将传递的参数使用"/"拼写在路由地址后面添加占位符即可,由于地址是字符串形式,所以所有的参数都会被解析成字符串,可以使用arguments来为参数指定type类型,它接收 NamedNavArgument 类型的列表,可通过 navArgument() 创建元素。
上面将参数ParamsConfig.PARAMS_AGE指定为了Int类型,参数ParamsConfig.PARAMS_NAME是String类型,可以不用额外指定,当然,navArgument(ParamsConfig.PARAMS_NAME) {}这句不写也是可以的。
提取参数
通过composable函数中提供的NavBackStackEntry提取这些参数,跳转时将参数添加到路线中。
修改PageOne页面跳转PageTwo时携带参数:
ini
@Composable
fun PageOne(navController: NavController) {
Column(
modifier = Modifier
.fillMaxSize()
.background(
Color.White
)
) {
Text(text = "这是页面1")
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = {
//普通跳转
// navController.navigate(RouteConfig.ROUTE_PAGE_TWO)
//携带参数跳转,必传参数必须传,不传会crash
navController.navigate("${RouteConfig.ROUTE_PAGE_TWO}/this is name/12")
}) {
Text(
text = "跳转页面2",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
}
}
}
修改PageTwo页面接受传递的参数,并添加一个Text用于显示
ini
@Composable
fun PageTwo(name: String?, age: Int, navController: NavController) {
Column(
modifier = Modifier
.fillMaxSize()
.background(
Color.White
)
) {
Text(text = "这是页面2")
Text(text = "name:$name,age:$age")
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = {
navController.navigate(RouteConfig.ROUTE_PAGE_THREE)
}) {
Text(
text = "跳转页面3",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
}
}
}
如果参数不传或者少传一个会怎么样呢?会直接crash,因为占位符的方式相当于必传参数,如果不传的话则会抛出异常。
可选参数
可选参数使用"?argName={argName}&argName2={argName2}"这种方式拼接在路由地址后面添加占位符。跟浏览器地址栏的可选参数一样,第一个用?拼接,后续用&拼接。
修改NavHost中PageThree的路由,使之拼接可选参数:
kotlin
//可选参数,使用"?argName={argName}&argName2={argName2}"拼接,跟浏览器地址栏的可选参数一样,第一个用?拼接,后续用&拼接
composable("${RouteConfig.ROUTE_PAGE_THREE}?${ParamsConfig.PARAMS_NAME}={${ParamsConfig.PARAMS_NAME}}&${ParamsConfig.PARAMS_AGE}={${ParamsConfig.PARAMS_AGE}}",
arguments = listOf(
navArgument(ParamsConfig.PARAMS_NAME) {
nullable = true
},
navArgument(ParamsConfig.PARAMS_AGE) {
type = NavType.IntType //指定具体类型
defaultValue = 25 //默认值(选配)
nullable = false //可否为null(选配)
}
)) {
//通过composable函数中提供的NavBackStackEntry提取参数
val argument = requireNotNull(it.arguments)
val name = argument.getString(ParamsConfig.PARAMS_NAME)
val age = argument.getInt(ParamsConfig.PARAMS_AGE)
PageThree(name, age, navController)
}
上面将参数ParamsConfig.PARAMS_NAME和ParamsConfig.PARAMS_AGE都设置成了可选参数,ParamsConfig.PARAMS_NAME参数可空,ParamsConfig.PARAMS_AGE不可空,其默认值是25。 参数提取方式同必传参数。
将PageThree页面修改成PageTwo页面一样接受传递的参数,并添加一个Text用于显示。
bash
Text(text = "name:$name,age:$age")
当跳转到PageThree页面没有参数时:
scss
navController.navigate(RouteConfig.ROUTE_PAGE_THREE)
ParamsConfig.PARAMS_NAME参数null,而ParamsConfig.PARAMS_AGE参数使用其默认值25:
csharp
name:null,age:25
当跳转到PageThree页面携带参数时:
bash
navController.navigate("${RouteConfig.ROUTE_PAGE_THREE}?${ParamsConfig.PARAMS_NAME}=demo&${ParamsConfig.PARAMS_AGE}=15")
结果为:
makefile
name:demo,age:15
深度链接 DeepLink
应用内跳转
深度链接可以响应其他界面或外部APP的跳转,当其他应用触发该深度链接时 Navigation 会自动深度链接到相应的可组合项。composable() 的 deepLinks 参数接收 NavDeepLink 类型的列表,可通过 navDeepLink() 创建元素。
ini
const val URI = "my-app://my.example.app"
//深度链接 DeepLink
composable("${RouteConfig.ROUTE_PAGE_FOUR}?${ParamsConfig.PARAMS_NAME}={${ParamsConfig.PARAMS_NAME}}&${ParamsConfig.PARAMS_AGE}={${ParamsConfig.PARAMS_AGE}}",
arguments = listOf(
navArgument(ParamsConfig.PARAMS_NAME) {
nullable = true
},
navArgument(ParamsConfig.PARAMS_AGE) {
type = NavType.IntType //指定具体类型
defaultValue = 25 //默认值(选配)
nullable = false //可否为null(选配)
}
),
deepLinks = listOf(navDeepLink {
uriPattern = "$URI/{${ParamsConfig.PARAMS_NAME}}/{${ParamsConfig.PARAMS_AGE}}"
})
) {
//通过composable函数中提供的NavBackStackEntry提取参数
val argument = requireNotNull(it.arguments)
val name = argument.getString(ParamsConfig.PARAMS_NAME)
val age = argument.getInt(ParamsConfig.PARAMS_AGE)
PageFour(name, age, navController)
}
参数解析同上面的携带参数跳转,在deepLinks参数中添加深度链接的模式匹配。 深度链接进行跳转:
kotlin
//深度链接匹配跳转
navController.navigate("$URI/deeplink/123".toUri())
响应外部跳转
默认情况下,深度链接不会向外部应用公开。如需向外部提供这些深层链接,必须向应用的 manifest.xml 文件添加相应的元素。在清单的元素中添加以下内容:
xml
<activity ...>
<intent-filter>
...
<data android:scheme="my-app" android:host="my.example.app" />
</intent-filter>
</activity>
Navigation搭配底部导航栏
这边只贴一下关键代码,具体代码可下载demo项目查看。
需要注意的是,如果当前不是在首页(home)tab页面,而是切换到其他tab页面,那么此时按back键它会先返回到首页 (home)tab 页面, 再按一次back键才会退出。解决方案就是在navController.navigate方法之前调用了一次 navController.popBackStack(),即先弹一次回退栈,这样就正常了。
kotlin
@Composable
fun BottomBar(
navController: NavHostController,
items: List<Screen>, //导航路线
modifier: Modifier = Modifier
) {
//获取当前的 NavBackStackEntry 来访问当前的 NavDestination
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
Row(
modifier = modifier.background(color = Color.White),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically
) {
items.forEachIndexed { index, screen ->
BottomBarItem(
item = screen,
//与层次结构进行比较来确定是否被选中
isSelected = currentDestination?.hierarchy?.any { it.route == screen.route },
onItemClicked = {
//加这个可解决问题:按back键会返回2次,第一次先返回home, 第二次才会退出
navController.popBackStack()
//点击item时,清空栈内 popUpTo ID到栈顶之间的所有节点,避免站内节点持续增加
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) {
//跳转时保存页面状态
saveState = true
}
//栈顶复用,避免重复点击同一个导航按钮,回退栈中多次创建实例
launchSingleTop = true
//回退时恢复页面状态
restoreState = true
//通过使用 saveState 和 restoreState 标志,当在底部导航项之间切换时,
//系统会正确保存并恢复该项的状态和返回堆栈。
}
}
)
}
}
}