一、概念
| NavController 控制器 | 用于在目的地之间跳转、处理深层链接、管理返回栈等。通过 rememberNavController() 获取。 |
| NavHost 宿主 | |
| NavGraph 导航图 | 一种数据结构,用于定义并连接节点。通过 navController.createGraph() 创建,NavHost的最后一行 Lambda 会传递给该方法来创建。 |
| NavDestination 目的地 | 导航图中的节点,可以是嵌套图(通过navigation()创建)、界面(通过composable()创建)、对话框(通过dialog()创建,悬浮在上一个界面的上方)。 |
1.1 控制器 NavController
| @Composable public expect fun rememberNavController( vararg navigators: Navigator<out NavDestination> ): NavHostController |
val navController = rememberNavController() //获取控制器。
1.2 宿主 NavHost
- 会出现跳转的界面显示异常(全白或者LazyColumn显示不全,特别是在使用Pager的时候),重启AS没用的话,给 NavHost 设置屏幕级背景色。
| @Composable //绑定控制器 //起始页 //入场动画 //出场动画 //构建导航图 |
NavHost(
modifier = Modifier.fillMaxSize(),
navController = navController,
startDestination = MainScreenRoute.Home,
) {
//构建导航图...
}
1.3 导航图 NavGraph
NavHost 的最后一行 Lambda 会自动传递给该方法来创建。也可单独创建后传递给 NavHost。
| public inline fun NavController.createGraph( startDestination: Any, //起始页 route: KClass<*>? = null, typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> = emptyMap(), builder: NavGraphBuilder.() -> Unit ): NavGraph |
| @Composable public fun NavHost( navController: NavHostController, //绑定控制器 graph: NavGraph, //绑定导航图 modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.TopStart, enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) = { fadeIn(animationSpec = tween(700)) }, exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) = { fadeOut(animationSpec = tween(700)) }, popEnterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) = enterTransition, popExitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) = exitTransition, sizeTransform: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> SizeTransform?)? = null ) |
val navGraph = remember(navController) {
navController.createGraph(startDestination = MyScreenRoute.Msg) {
composable<MyScreenRoute.Msg> { MsgScreen() }
composable<MyScreenRoute.Msg> { MsgScreen() }
}
}
NavHost(navController = navController, graph = navGraph)
1.4 目的地 NavDestination
| 界面 | public inline fun <reified T : Any> NavGraphBuilder.composable( //深层链接 //进场动画 //出场动画 //目标可组合项 |
| 对话框 | public inline fun <reified T : Any> NavGraphBuilder.dialog( typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> = emptyMap(), deepLinks: List<NavDeepLink> = emptyList(), dialogProperties: DialogProperties = DialogProperties(), noinline content: @Composable (NavBackStackEntry) -> Unit ) |
| 嵌套图 | public inline fun <reified T : Any> NavGraphBuilder.navigation( startDestination: Any, typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> = emptyMap(), noinline builder: NavGraphBuilder.() -> Unit ): Unit |
NavHost(...) {
//无参节点
composable<MyScreenRoute.Msg> { MsgScreen() }
//有参节点
composable<MyScreenRoute.Profile> { navBackStackEntry ->
val profile: MyScreenRoute.Profile = navBackStackEntry.toRoute()
ProfileScreen(profile.id)
}
//嵌套导航图
navigation<MyScreenRoute.Home>(MyScreenRoute.Home.Hot) {
composable<MyScreenRoute.Home.Hot> { HomeHotScreen() }
composable<MyScreenRoute.Home.Square> { HomeSquareScreen() }
}
//对话框节点
dialog<MyScreenRoute.Msg> { MsgScreen() }
}
二、使用方式一(v2.8 版本支持类型安全路径,推荐)
2.1 添加依赖
在模块的 build.gradle 中添加插件和依赖。
[versions]
kotlinSerialization = "2.2.10"
kotlinSerializationJson = "1.9.0"
[libraries]
kotlin-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinSerializationJson" }
[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization" }
2.2 定义路径
sealed interface MyScreenRoute{
//无参
@Serializable
data object Msg : MyScreenRoute
//有参
@Serializable
data class Profile( val id: Long) : MyScreenRoute
//嵌套导航图
@Serializable
sealed interface Home : MyScreenRoute {
@Serializable data object Hot : Home
@Serializable data object Square : Home
}
}
2.3 定义容器&导航图
@Composable
fun SwitchRegion(
navController: NavHostController = rememberNavController(), //提供默认实现(如果外部没有调用跳转就不用传了)
) {
NavHost(
modifier = Modifier.fillMaxSize(),
navController = navController,
startDestination = MainScreenRoute.Home,
) {
//无参节点
composable<MyScreenRoute.Msg> { MsgScreen() }
//有参节点
composable<MyScreenRoute.Profile> { navBackStackEntry ->
//获取参数
val profile: MyScreenRoute.Profile = navBackStackEntry.toRoute()
ProfileScreen(profile.id)
}
//嵌套导航图
navigation<MyScreenRoute.Home>(MyScreenRoute.Home.Hot) {
composable<MyScreenRoute.Home.Hot> { HomeHotScreen() }
composable<MyScreenRoute.Home.Square> { HomeSquareScreen() }
}
//对话框节点
dialog<MyScreenRoute.Msg> { MsgScreen() }
}
}
2.4 跳转
navController.navigate(route = MyScreenRoute.Profile(id = 123))
三、使用方式二(字符串路径,传参困难)
由于 NavController 和 NavHost 可以有很多个,NavHost 中的界面要跳转的话,需要使用自己锁绑定的 NavController,不能混用。跳转通过调用 navController.navigate()。
3.1 定义节点配置
使用配置文件便于管理路线名称。
object RouteConfig {
const val PAGE_ONE = "pageOne"
const val PAGE_TWO = "pageTwo"
const val PAGE_THREE = "pageThree"
}
3.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" ) } //子界面中也有跳转时,将逻辑抛给外部
}
}
3.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) } //实现子界面中的跳转
)
}
}
}
3.4 调用跳转
默认情况下,navigate() 会将目的地添加到回退栈中,可通过 popUpTo() 在跳转前将当前位置到回退栈中的目标位置之间的 NavBackStackEntry 全部弹出来避免添加过多的回退,配置 inclusive = true 包含目标位置也弹出,还可以配置 launchSingleTop = true 实现栈顶复用。
- 用了 inclusive 就要用 launchSingleTop,否则被跳转的页面会挂载两次(2023-12-13)
// 回退栈会弹出当前位置到"home"之间的所有界面,再进入"friendslist"界面。
navController.navigate("friendslist") {
popUpTo("home")
}
// 回退栈会弹出当前位置到"home"之间的所有界面,包括”home“,再进入"friendsList"界面。
navController.navigate("friendslist") {
popUpTo("home") { inclusive = true }
}
// 对应 Android 的 SingleTop,如果回退栈顶部已经是 "search",就不会重新创建。
navController.navigate("search") {
launchSingleTop = true
}
// 对应 Android 的 SingleTask,如果回退栈里已经有 "home",就会弹出它上面的所有界面。
//方式一
navController.navigate("home") {
popUpTo("home") { inclusive = true }
}
//方式二
navController.navigate("home") {
popUpTo("home")
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") }
}
}
3.5 返回
//返回上一级界面(推荐)
navController.navigateUp()
//可以指定返回的界面,不指定就相当于navigateUp()
//最后一个界面弹出显示空白页面,通过返回的Boolean=false将activity给finish()掉
navController.popBackStack().also {
if (!it){
LocalActivity.current?.finish()
}
}
3.6 携带参数跳转
界面跳转应该传递最少的必要的数据(如唯一标识符ID),需要传递复杂的数据,应该将这些数据保存在数据层,跳转到新界面后根据ID到数据层获取。通过路线处理参数的结构意味着组合将完全独立于 Navigation 并且更易于测试。
3.6.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.6.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"}")
3.7 嵌套导航图
导航图中嵌套导航图,将目的地设为另一张图来对特定流程进行模块化。在 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)
}
四、深层链接 Deep Link
深层链接可以响应其他界面或外部APP的跳转,当其他应用触发该深层链接时 Navigation 会自动深层链接到相应的可组合项。composable() 的 deepLinks 参数接收 NavDeepLink 类型的列表,可通过 navDeepLink() 创建元素。
4.1 本应用内跳转
val uri = "https://www.example.com"
composable(
"profileScreen?id={id}",
deepLinks = listOf(navDeepLink { uriPattern = "$uri?userId={id}" })
) { backStackEntry ->
val id = backStackEntry.arguments?.getString("id")
ProfileScreen(id)
}
4.2 响应外部跳转
默认情况下,深层链接不会向外部公开,需要向 Manifest 中添加相应的 <intent-filter>。
<activity …>
<intent-filter>
...
<data android:scheme="myapp" android:host="profileScreen" />
</intent-filter>
</activity>
composable(
"profileScreen/{id}",
deepLinks = listOf(navDeepLink { uriPattern = "myapp://profileScreen/{id}" })
) { backStackEntry ->
val id = backStackEntry.arguments?.getString("id")
ProfileScreen(id)
}
navController.navigate("myapp://profileScreen/123456")
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)
}
六、与底部导航栏集成
@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
})
}
文章详细介绍了JetpackCompose中NavController和NavHost的使用,包括如何创建和管理页面堆栈,以及如何处理页面跳转、参数传递和深链接。还提到了与底部导航栏的集成,以及与Hilt结合使用的方法,提供了示例代码来展示具体实现。
2303

被折叠的 条评论
为什么被折叠?



