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 标志,当在底部导航项之间切换时,
                        //系统会正确保存并恢复该项的状态和返回堆栈。
                    }
                }
            )
        }
    }
}
相关推荐
Mr Lee_34 分钟前
android 配置鼠标右键快捷对apk进行反编译
android
顾北川_野1 小时前
Android CALL关于电话音频和紧急电话设置和获取
android·音视频
&岁月不待人&1 小时前
Kotlin by lazy和lateinit的使用及区别
android·开发语言·kotlin
Winston Wood3 小时前
Android Parcelable和Serializable的区别与联系
android·序列化
清风徐来辽3 小时前
Android 项目模型配置管理
android
帅得不敢出门4 小时前
Gradle命令编译Android Studio工程项目并签名
android·ide·android studio·gradlew
problc4 小时前
Flutter中文字体设置指南:打造个性化的应用体验
android·javascript·flutter
帅得不敢出门15 小时前
安卓设备adb执行AT指令控制电话卡
android·adb·sim卡·at指令·电话卡
我又来搬代码了16 小时前
【Android】使用productFlavors构建多个变体
android
德育处主任18 小时前
Mac和安卓手机互传文件(ADB)
android·macos