Compose Navigation使用

如果使用纯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

应用内跳转

深度链接可以响应其他界面或外部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>

这边只贴一下关键代码,具体代码可下载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 标志,当在底部导航项之间切换时,
                        //系统会正确保存并恢复该项的状态和返回堆栈。
                    }
                }
            )
        }
    }
}
相关推荐
雨白3 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹5 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空7 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭7 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日8 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安8 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑8 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟12 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡14 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi0014 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体