一、概念
1.1 控制器 NavController
维护了 Navigation 内部关于页面的堆栈、状态信息、导航图。
val navController = rememberNavController() //获取控制器。
1.2 容器 NavHost
内部持有 NavController,在页面切换时渲染UI。通过 composable() 构建路线(节点)。
- 会出现跳转的界面显示异常(全白或者LazyColumn显示不全,特别是在使用Pager的时候),重启AS没用的话,给 NavHost 设置屏幕级背景色。
public fun NavHost( navController: NavHostController, //绑定 NavController startDestination: String, //起始页 modifier: Modifier = Modifier, route: String? = null, //用于不同的 NavHost 间跳转 builder: NavGraphBuilder.() -> Unit //构建导航图 ) |
public fun NavGraphBuilder.composable( arguments: List<NamedNavArgument> = emptyList(), //获取上个界面跳转携带的参数 |
二、简单使用
由于 NavController 和 NavHost 可以有很多个,NavHost 中的界面要跳转的话,需要使用自己锁绑定的 NavController,不能混用。跳转通过调用 navController.navigate()。
2.1 定义节点配置
使用配置文件便于管理路线名称。
object RouteConfig {
const val PAGE_ONE = "pageOne"
const val PAGE_TWO = "pageTwo"
const val PAGE_THREE = "pageThree"
}
2.2 创建被跳转的界面
被跳转的界面需要调用跳转,将跳转抽取成函数参数。
//定义三个页面
@Composable
fun PageOne() {
Box(modifier = Modifier.background(Color.Blue).fillMaxSize().wrapContentSize(align = Alignment.Center)) {
Text(text = "Page One", fontSize = 100.sp, color = Color.White)
}
}
@Composable
fun PageTwo() {
Box(modifier = Modifier.background(Color.Green).fillMaxSize().wrapContentSize(align = Alignment.Center)) {
Text(text = "Page Two", fontSize = 100.sp, color = Color.White)
}
}
@Composable
fun PageThree(onclicked: () -> Unit) {
Column(modifier = Modifier.background(Color.Red).fillMaxSize().wrapContentSize(align = Alignment.Center)) {
Text(text = "Page Three", fontSize = 100.sp, color = Color.White)
Button(onClick = { onclicked() }){ Text( text = "界面3跳转到界面1" ) } //子界面中也有跳转时,将逻辑抛给外部
}
}
2.3 创建用于显示的容器
@Composable
fun SwitchRegion(
navController: NavHostController = rememberNavController(), //提供默认实现(如果外部没有调用跳转就不用传了)
startDestination: String = RouteConfig.PAGE_ONE,
modifier: Modifier = Modifier
) {
NavHost(
navController = navController,
startDestination = startDestination,
modifier = modifier
) {
composable(route = RouteConfig.PAGE_ONE) { PageOne() }
composable(route = RouteConfig.PAGE_TWO) { PageTwo() }
composable(route = RouteConfig.PAGE_THREE) {
PageThree(
onClick = { navController.navigate(RouteConfig.PAGE_ONE) } //实现子界面中的跳转
)
}
}
}
2.4 调用跳转
默认情况下,navigate() 会将目的地添加到回退栈中,可通过 popUpTo() 在跳转前将当前位置到回退栈中的目标位置之间的 NavBackStackEntry 全部弹出来避免添加过多的回退,配置 inclusive = true 包含目标位置也弹出,还可以配置 launchSingleTop = true 实现栈顶复用。
- 用了 inclusive 就要用 launchSingleTop,否则被跳转的页面会挂载两次(2023-12-13)
// 在进入"friendsList"之前,回退栈会弹出所有的可组合项,直到 "home"
navController.navigate("friendslist") {
popUpTo("home")
}
// 在进入"friendsList"之前,回退栈会弹出所有的可组合项,直到 "home",并且包括它
navController.navigate("friendslist") {
popUpTo("home") { inclusive = true }
}
// 对应 Android 的 SingleTop,如果回退栈顶部已经是 "search",就不会重新创建
navController.navigate("search") {
launchSingleTop = true
}
@Composable
fun Screen(
modifier: Modifier = Modifier
) {
val navController = rememberNavController()
Column(modifier = modifier.fillMaxSize(), verticalArrangement = Arrangement.Top) {
SwitchButton(
onButtonOneClick = { navController.navigate(RouteConfig.PAGE_ONE) },
onButtonTwoClick = { navController.navigate(RouteConfig.PAGE_TWO) },
onButtonThreeClick = { navController.navigate(RouteConfig.PAGE_THREE) }
)
SwitchRegion(navController = navController)
}
}
//三个按钮点击切换
@Composable
fun SwitchButton(
onButtonOneClick: () -> Unit,
onButtonTwoClick: () -> Unit,
onButtonThreeClick: () -> Unit,
modifier: Modifier = Modifier
) {
Row(modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
Button(onClick = onButtonOneClick) { Text(text = "Button One") }
Button(onClick = onButtonTwoClick) { Text(text = "Button Two") }
Button(onClick = onButtonThreeClick) { Text(text = "Button Three") }
}
}
2.5 返回
navController.navigateUp() //返回上一级界面
navController.popBackStack() //可以指定返回的界面(不指定就相当于navigateUp())。
三、携带参数跳转
界面跳转应该传递最少的必要的数据(如唯一标识符ID),需要传递复杂的数据,应该将这些数据保存在数据层,跳转到新界面后根据ID到数据层获取。通过路线处理参数的结构意味着组合将完全独立于 Navigation 并且更易于测试。
3.1 必传参数
直接将传递的数据名称使用 "/" 拼写在地址后面添加占位符即可,由于地址是字符串形式,使用 arguments 来指定数据的类型,它接收 NamedNavArgument 类型的列表,可通过 navArgument() 创建元素。从 composable() 的 Lambda 中提取这些参数。跳转时将参数添加到路线中。
//使用配置文件方便管理参数名称
object ParamConfig {
const val ID = "id"
const val NAME = "name"
}
NavHost(
navController = rememberNavController(),
//路径写法一:使用路线和参数配置文件
startDestination = "${RouteConfig.PAGE_ONE}/${ParamConfig.ID}/${ParamConfig.NAME}"
) {
composable(
//路径写法二:直接写
route = "pageOne/{id}/{name}", //向路线中添加占位符
arguments = listOf( //往集合中添加参数(NamedNavArgument类型)
navArgument(name = "id") {
type = NavType.LongType //指定具体类型
defaultValue = "123456" //默认值(选配)
nullable = false //可否为null(选配)
},
navArgument(name = ParamConfig.NAME) //参数是String类型可以不用额外指定
)
){ navBackStackEntry -> //从Lambda中提取参数
val arguments = requireNotNull(navBackStackEntry.arguments)
val id = arguments.getLong(ParamConfig.ID)
val name = arguments.getString(ParamConfig.NAME)
PageProfile(id, name!!)
}
}
//跳转时将参数添加到路线中
val id = 123456L
val name = "张三"
navController.navigate("${RouteConfig.PAGE_ONE}/$id/$name")
3.2 可选参数
必须使用查询参数语法来添加("?argName={argname}"),第一个是参数名,第二个是 key。必须具有 defaultValue集 或 nullablility = true(将默认值隐式设为null),这意味着所有可选参数都必须以列表的形式显示添加到 composable( ) 函数。多个可选参数之间用 & 隔开,例如:"?argName1={argName1}&argName2={argName2}",传递的字符串不要包含 / 符号。
composable(
route = "pageProfile?userId={userId}",
arguments = listOf(
navArgument("userId") {
defaultValue = "123456"
nullable = false
}
)
) { backStackEntry ->
backStackEntry.arguments?.getString("userId")
}
navController.navigate("pageProfile?userId=${"123456"}")
四、深层链接 Deep Link
深层链接可以响应其他界面或外部APP的跳转,当其他应用触发该深层链接时 Navigation 会自动深层链接到相应的可组合项。composable() 的 deepLinks 参数接收 NavDeepLink 类型的列表,可通过 navDeepLink() 创建元素。
4.1 本应用内跳转
val uri = "https://www.example.com"
composable(
"pageProfile?id={id}",
deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("id"))
}
4.2 响应外部跳转
默认情况下,深层链接不会向外部公开,需要向 Manifest 中添加相应的 <intent-filter>。
<activity …>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
4.3 PendingIntent
可以像使用任何其他 PendingIntent 一样,使用此 deepLinkPendingIntent 在相应深层链接目的地打开您的应用。
val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
Intent.ACTION_VIEW,
"https://www.example.com/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
五、嵌套导航
导航图中嵌套导航图,将目的地设为另一张图来对特定流程进行模块化。在 NavHost 中使用 navigation() 来配置,与根图一样嵌套图必须设置起始页。
无法跳转到嵌套导航图中的某个特定页面,只能跳转到它的起始页,这种特性使其适合用于封装特定流程的界面组合,比如登录和支付流程。
//一般写法
NavHost(navController, startDestination = "home") {
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中使用
NavHost(navController, startDestination = "home") {
loginGraph(navController)
}
六、与底部导航栏集成
@Composable
private fun BottomBar(
navController: NavHostController,
routes: List<String>, //导航路线
labels: Array<String>, //按钮名称
normalIcons: List<Int>, //未选中图标
selectedIcons: List<Int>, //选中图标
modifier: Modifier = Modifier
) {
//确保各项传入的数量一致
require(routes.size == labels.size && routes.size == normalIcons.size && routes.size == selectedIcons.size)
//获取当前的 NavBackStackEntry 来访问当前的 NavDestination
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
Row(modifier = modifier, horizontalArrangement = Arrangement.SpaceAround, verticalAlignment = Alignment.CenterVertically
){
routes.forEachIndexed { index, route ->
BottomBarItem(
label = labels[index],
normalIcon = normalIcons[index],
selectedIcon = selectedIcons[index],
//与层次结构进行比较来确定是否被选中
isSelected = currentDestination?.hierarchy?.any { it.route == route },
onItemClicked = {
navController.navigate(route) {
//当页面不在起始页时,按返回键回到起始页
popUpTo(navController.graph.findStartDestination().id) {
//跳转时保存页面状态
saveState = true
}
//栈顶复用,避免重复点击同一个导航按钮,回退栈中多次创建实例
launchSingleTop = true
//回退时恢复页面状态
restoreState = true
}
}
)
}
}
}
@Composable
private fun BottomBarItem(
label: String, //按钮名称
normalIcon: Int, //未选中图标
selectedIcon: Int, //选中图标
isSelected: Boolean?, //是否选中
onItemClicked: () -> Unit, //按钮点击监听
modifier: Modifier = Modifier
) {
Column(
//去除点击水波纹
modifier = modifier.clickable(indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = onItemClicked),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = modifier.size(30.dp),
painter = painterResource(id = if (isSelected == true) selectedIcon else normalIcon),
contentDescription = label,
tint = if (isSelected == true) AppTheme.colors.textPrimary else AppTheme.colors.textSecondary,
)
Text(
text = label,
color = if (isSelected == true) AppTheme.colors.textPrimary else AppTheme.colors.textSecondary,
fontSize = 10.sp,
)
}
}
七、与 Hilt 搭配使用
始终使用 hiltViewModel() 来获取带有 @HiltViewModel 注解的实例,该函数可以与带有 @AndroidEntryPoint 注解的 Activity/Fragment 搭配使用。
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
7.1 获取作用域限定为目的地的ViewModel实例
@Composable
fun Demo() {
NavHost(
navController = navController,
startDestination = "pageOne"
) {
composable("pageTwo") { navBackStackEntry->
val viewModel = hiltViewModel<DemoViewModel>()
PageTwoScreen(viewModel)
}
}
}
7.2 获取作用域限定为导航路线或导航图的ViewModel实例
使用 hiltViewModel() 将相应的 backStackEntry 作为参数传递。
@Composable
fun Demo() {
NavHost(
navController = navController,
startDestination = "Page1"
) {
navigation(startDestination = "Page2", route = "Page3") {
composable("Page4") { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry("Parent")
}
val parentViewModel = hiltViewModel<ParentViewModel>(parentEntry)
Page4Screen(parentViewModel)
}
}
}
}
八、监听返回键
BackHandler{
// 什么都不写就是:拦截返回键事件,不让它回退到上一个界面
// 返回桌面
context.startActivity(Intent(Intent.ACTION_MAIN).apply {
addCategory(Intent.CATEGORY_HOME)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}