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代码中进行控制,更加方便灵活了。
导航路由配置
NavController 是 Navigation 的核心,它是有状态的,可以跟踪返回堆栈以及每个界面的状态。可以通过 rememberNavController
来创建一个NavController
的实例。
NavHost 是导航容器,NavHost
将 NavController
与导航图相关联,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,