美文网首页
Compose Navigation使用

Compose Navigation使用

作者: 愿天深海 | 来源:发表于2024-01-11 12:44 被阅读0次

    如果使用纯Compose开发Android应用,在页面导航方面navigation-compose几乎是唯一选择。
    介绍一下navigation-compose的简单使用。
    本篇文章Demo下载

    依赖

    implementation("androidx.navigation:navigation-compose:2.7.5")
    

    普通跳转

    首先需要获取控制器NavController:

    val navController = rememberNavController()
    

    NavController维护了Navigation 内部关于页面的堆栈、状态信息、导航图。
    然后需要一个NavHost对象,定义导航的入口,同时也是承载导航页面的容器。
    NavHost内部持有 NavController,在页面切换时渲染UI。通过 composable() 构建路线(节点)。

        @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是用于方便管理路由地址和路由参数等的配置文件:
    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。

        @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:

        @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:传入一个目的地,在跳转新页面之前会将页面堆栈中直到目的地的所有可组合项弹出,然后跳转新页面。
        // 在进入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 会将目的地也弹出。
        // 在进入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,实现栈顶复用
        // 对应 Android 的 SingleTop,如果回退栈顶部已经是 RouteConfig.ROUTE_PAGE_THREE,就不会重新创建
        navController.navigate(RouteConfig.ROUTE_PAGE_THREE) {
            launchSingleTop = true
        }
    

    返回

    navigateUp方法可以返回到上一级页面,popBackStack可以返回到指定页面,如果不指定页面,就是将当前页面弹出,也就是返回上一级页面,同navigateUp。

        @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
                    )
                }
            }
        }
    

    携带参数跳转

    使用配置文件方便管理参数名称:

    object ParamsConfig {
    
        /**
         * 参数-name
         */
        const val PARAMS_NAME = "name"
    
        /**
         * 参数-age
         */
        const val PARAMS_AGE = "age"
    }
    

    必传参数

    修改NavHost中PageTwo的路由,使之需要携带参数:

                //必传参数,使用"/"拼写在路由地址后面添加占位符
                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时携带参数:

        @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用于显示

        @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的路由,使之拼接可选参数:

                //可选参数,使用"?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用于显示。

    Text(text = "name:$name,age:$age")
    

    当跳转到PageThree页面没有参数时:

    navController.navigate(RouteConfig.ROUTE_PAGE_THREE)
    

    ParamsConfig.PARAMS_NAME参数null,而ParamsConfig.PARAMS_AGE参数使用其默认值25:

    name:null,age:25
    

    当跳转到PageThree页面携带参数时:

    navController.navigate("${RouteConfig.ROUTE_PAGE_THREE}?${ParamsConfig.PARAMS_NAME}=demo&${ParamsConfig.PARAMS_AGE}=15")
    

    结果为:

    name:demo,age:15
    

    深度链接 DeepLink

    应用内跳转

    深度链接可以响应其他界面或外部APP的跳转,当其他应用触发该深度链接时 Navigation 会自动深度链接到相应的可组合项。composable() 的 deepLinks 参数接收 NavDeepLink 类型的列表,可通过 navDeepLink() 创建元素。

                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参数中添加深度链接的模式匹配。
    深度链接进行跳转:

                    //深度链接匹配跳转
                    navController.navigate("$URI/deeplink/123".toUri())
    

    响应外部跳转

    默认情况下,深度链接不会向外部应用公开。如需向外部提供这些深层链接,必须向应用的 manifest.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(),即先弹一次回退栈,这样就正常了。

    @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 标志,当在底部导航项之间切换时,
                            //系统会正确保存并恢复该项的状态和返回堆栈。
                        }
                    }
                )
            }
        }
    }
    

    相关文章

      网友评论

          本文标题:Compose Navigation使用

          本文链接:https://www.haomeiwen.com/subject/dwjvgdtx.html