Compose - 使用 Navigation

点击跳转 普通使用方式

一、概念

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(
    route: String,        //路线名称

    arguments: List<NamedNavArgument> = emptyList(),        //获取上个界面跳转携带的参数
    deepLinks: List<NavDeepLink> = emptyList(),        //深层链接
    content: @Composable (NavBackStackEntry) -> Unit        //页面的UI代码
)

二、简单使用

由于 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
    })
}

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值