Jetpack Compose中的导航路由

Jetpack Compose中的导航库是由Jetpack库中的Navigation组件库的基础上添加的对Compose的扩展支持,使用需要单独添加依赖:

implementation "androidx.navigation:navigation-compose:$nav_version" 

Jetpack库中的Navigation使用起来还是比较麻烦的,首先需要在xml中进行导航图的配置,然后在代码中使用NavController.navigate(id)进行跳转到指定的id的fragment页面,个人感觉这种方式还是不够灵活,需要预先定义,假如某个fragment没有在xml中定义就无法使用NavController进行跳转,另外还需要在xml和java/kotlin文件来回折腾修改。

Jetpack Compose中的Navigation在功能上跟jetpack组件库中对Fragment的导航使用方式很类似,但是使用Compose的好处是,它是纯kotlin的代码控制,不需要在xml再去配置,一切都是在kotlin代码中进行控制,更加方便灵活了。

导航路由配置

NavControllerNavigation 的核心,它是有状态的,可以跟踪返回堆栈以及每个界面的状态。可以通过 rememberNavController 来创建一个NavController的实例。

NavHost 是导航容器,NavHostNavController 与导航图相关联,NavController 能够在所有页面之间进行跳转。当在进行页面跳转时,NavHost 的内容会自动进行重组。导航图中的目的地就是一个路由。路由名称通常是一个字符串。

@Composable
fun NavigationExample() {
   
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = "Welcome") {
   
        composable("Welcome") {
    WelcomeScreen(navController) }
        composable("Login") {
    LoginScreen(navController) }
        composable("Home") {
    HomeScreen(navController) }
        composable("Cart") {
    CartScreen(navController) }
    }
}

NavHost 中通过composable(routeName){...}进行路由地址和对应的页面进行配置,startDestination 指定的路由地址将作为首页进行展示。

导航路由跳转

路由跳转就是通过navController.navigate(id)的方式进行跳转,id参数就是前面配置的目标页面的路由地址。

@Composable
fun WelcomeScreen(navController : NavController) {
   
    Column() {
   
        Text("WelcomeScreen", fontSize = 20.sp)
        Button(onClick = {
    navController.navigate("Login") }) {
   
            Text(text = "Go to LoginScreen")
        }
    }
}

注意: 实际业务中,路由名称的字符串应当全部改成密封类的实现方式。

这种方式是将 navController 作为参数传入到了Composable组件中进行调用,更加优雅的方式应当是通过函数回调的方式,来进行跳转,不用每个都传一个navController参数:

@Composable
fun NavigationExample2() {
   
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = "Welcome") {
   
        composable("Welcome") {
   
            WelcomeScreen {
   
                navController.navigate("Login")
            }
        }
        ...
    }
}
@Composable
fun WelcomeScreen(onGotoLoginClick: () -> Unit = {
   }) {
   
    Column() {
   
        Text("WelcomeScreen", fontSize = 20.sp)
        Button(onClick = onGotoLoginClick) {
   
            Text(text = "Go to LoginScreen")
        }
    }
}

这种方式的好处是,更加易于复用和测试。

默认navigate是在回退栈中压入一个新的Compasable的Destination作为栈顶节点进行展示,可以选择在调用navigate方法时,在后面紧跟一个block lambda,在其中添加对NavOptions的操作。

 // 在跳转到 Home 之前 ,清空回退栈中Welcome之上到栈顶的所有页面(不包含Welcome)
 navController.navigate("Home"){
   
    popUpTo("Welcome")
 }

 // 同上,包含Welcome
 navController.navigate("Home"){
   
    popUpTo("Welcome"){
    inclusive = true }
 }

 // 当前栈顶已经是Home时,不再入栈新的Home节点,相当于Activity的SingleTop启动模式
 navController.navigate("Home"){
   
    launchSingleTop = true
 }

可以根据需求场景进行选择,例如从欢迎页面到登录页面,登录成功之后,跳转到首页,此时回退栈中首页之前的页面就不再需要了,按返回键可以直接返回桌面,这时就适合用下面代码进行跳转:

navController.navigate("Home") {
   
    popUpTo("Welcome") {
    inclusive = true}
}

另外,需要注意的一点是,如果跳转的目标路由地址不存在时,NavController会直接抛出IllegalArgumentException异常,导致应用崩溃,因此在执行navigate方法时我们应该进行异常捕获,并给出用户提示:

@Composable
fun NavigationExample2() {
   
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = "Welcome") {
   
        composable("Login") {
   
            val context = LocalContext.current
            LoginScreen {
   
                try {
   
                    navController.navigate("Home") {
   
                        popUpTo("Welcome") {
    inclusive = true}
                    }
                } catch (e : IllegalArgumentException) {
   
                    // 路由不存在时会抛异常
                    Log.e("TAG", "NavigationExample2: $e")
                    with(context) {
    showToast("Home路由不存在!")}
                }
            }
        }
        ...
    }
}

最好是封装一下定义一个扩展函数来使用,例如

fun NavHostController.navigateWithCall(
    route: String,
    onNavigateFailed: ((IllegalArgumentException)->Unit)?,
    builder: NavOptionsBuilder.() -> Unit
) {
   
    try {
   
        this.navigate(route, builder)
    } catch (e : IllegalArgumentException) {
   
        onNavigateFailed?.invoke(e)
    }
}
// 使用:
LoginScreen {
   
     navController.navigateWithCall(
         route = "Home",
         onNavigateFailed = {
    with(context) {
    showToast("Home路由不存在!")} }
     ) {
   
         popUpTo("Welcome") {
    inclusive = true}
     }
 }

导航路由传参

基本数据类型的传参

基本数据类型的参数传递是通过List/{userId}这种字符串模板占位符的方式来提供:

@Composable
fun NavigationWithParamsExample() {
   
    val navController = rememberNavController()
    NavHost(navController, startDestination = "Home") {
   
        composable("Home") {
   
            HomeScreen1 {
    userId, isFromHome ->
                 navController.navigate("List/$userId/$isFromHome")
            }
        }
        composable(
            "List/{userId}/{isFromHome}",
            arguments = listOf(
                navArgument("userId") {
    type = NavType.IntType }, // 设置参数类型
                navArgument("isFromHome") {
   
                    type = NavType.BoolType
                    defaultValue = false // 设置默认值
                }
            )
        ) {
    backStackEntry ->
            val userId = backStackEntry.arguments?.getInt("userId") ?: -1
            val isFromHome = backStackEntry.arguments?.getBoolean("isFromHome") ?: false
            ListScreen(userId, isFromHome) {
    id ->
                navController.navigate("Detail/$id")
            }
        }  
        composable("Detail/{detailId}") {
    backStackEntry ->
            val detailId = backStackEntry.arguments?.getString("detailId")
            DetailScreen(detailId) {
   
                navController.popBackStack()
            }
        }
    }
}

如上,在接受页面的路由配置中可以通过 arguments 参数接受一个 navArgument 的 List 集合, 通过navArgument 可以配置路由参数的类型和默认值等。但是如果参数过多,还要指定类型的话,明显就比较麻烦了,还不如传统的Intent传参方便。目前官方的api也没有提供其他的方式可以解决,所以最好的方式是将参数全部按照String类型进行传递,不指定具体的参数类型,在目标页面接受之后再进行转换。

可选参数

通过路由名称中以斜杠方式提供的参数,如果启动方不传会导致崩溃,可以通过路由名称后面跟 的方式提供可选参数,可选参数可以不传,不会导致崩溃。跟浏览器地址栏的可选参数一样。

例如:

navController.navigate("List2/$userId?fromHome=$isFromHome")
navController.navigate("List2/$userId") // 可以不传$isFromHome

接受方:

composable(
    "List2/{userId}?fromHome={isFromHome}", // 设置可选参数时,必须提供默认值
     arguments = listOf(
         navArgument("userId") {
    type = NavType.IntType },
         navArgument("isFromHome") {
   
             type = NavType.BoolType
             defaultValue = false
         }
     )
 ) {
    backStackEntry ->
     val userId = backStackEntry.arguments?.getInt("userId") ?: -1
     val isFromHome = backStackEntry.arguments?.getBoolean("isFromHome") ?: false
     ListScreen(userId
  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值