Jetpack Compose中的导航库是由Jetpack库中的Navigation 组件库的基础上添加的对Compose的扩展支持,使用需要单独添加依赖:
1 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 的内容会自动进行重组。导航图中的目的地就是一个路由。路由名称通常是一个字符串。
1 2 3 4 5 6 7 8 9 10 @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参数就是前面配置的目标页面的路由地址。
1 2 3 4 5 6 7 8 9 @Composable fun WelcomeScreen (navController : NavController ) { Column() { Text("WelcomeScreen" , fontSize = 20. sp) Button(onClick = { navController.navigate("Login" ) }) { Text(text = "Go to LoginScreen" ) } } }
注意: 实际业务中,路由名称的字符串应当全部改成密封类 的实现方式。
这种方式是将 navController 作为参数传入到了Composable组件中进行调用,更加优雅的方式应当是通过函数回调 的方式,来进行跳转,不用每个都传一个navController参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @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的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 navController.navigate("Home" ){ popUpTo("Welcome" ) } navController.navigate("Home" ){ popUpTo("Welcome" ){ inclusive = true } } navController.navigate("Home" ){ launchSingleTop = true }
可以根据需求场景进行选择,例如从欢迎页面到登录页面,登录成功之后,跳转到首页,此时回退栈中首页之前的页面就不再需要了,按返回键可以直接返回桌面,这时就适合用下面代码进行跳转:
1 2 3 navController.navigate("Home" ) { popUpTo("Welcome" ) { inclusive = true } }
另外,需要注意的一点是,如果跳转的目标路由地址不存在时,NavController会直接抛出IllegalArgumentException异常,导致应用崩溃,因此在执行navigate方法时我们应该进行异常捕获,并给出用户提示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @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路由不存在!" )} } } } ... } }
最好是封装一下定义一个扩展函数来使用,例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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}这种字符串模板占位符的方式来提供:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @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类型进行传递,不指定具体的参数类型,在目标页面接受之后再进行转换。
可选参数 通过路由名称中以斜杠方式提供的参数,如果启动方不传会导致崩溃,可以通过路由名称后面跟 ?的方式提供可选参数,可选参数可以不传,不会导致崩溃。跟浏览器地址栏的可选参数一样。
例如:
1 2 navController.navigate("List2/$userId ?fromHome=$isFromHome " ) navController.navigate("List2/$userId " )
接受方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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, isFromHome) { id -> navController.navigate("Detail/$id " ) } }
设置可选参数时,接受方必须提供默认值参数配置。
对象类型的传参 对于数据类或普通class对象类型的参数传递,首先想到的是传递序列化对象,但是很遗憾,官方目前还不支持对象类型的参数传递,虽然如此,但是很奇怪的是,你可以通过代码写出序列化的传参方式,例如以下通过Parcelable序列化的方式传参:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Parcelize data class User (val userId : Int , val name : String): Parcelable@Composable fun NavigationWithParamsExample () { val navController = rememberNavController() NavHost(navController, startDestination = "Home" ) { composable("Home" ) { HomeScreen1 { userId, isFromHome -> val user = User(56789 , "小明" ) navController.navigate("List3/$user " ) } } composable( "List3/{user}" , arguments = listOf( navArgument("user" ) { type = NavType.ParcelableType(User::class .java) }, ) ) { backStackEntry -> val user : User? = backStackEntry.arguments?.getParcelable("user" ) user?.run { ListScreen(userId, true ) { id -> navController.navigate("Detail/$id " ) } } } } }
以上代码虽然编译完全没有问题,但如果尝试运行以上代码,则会直接崩溃:
因为Compose的导航是基于Navigation的Deeplinks方式实现的,而Deeplinks参数目前不支持对象类型,只能传String字符串。
同样,以下通过Serializable序列化方式的传参也会崩溃,会报同样的错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 data class User2 (val userId : Int , val name : String): java.io.Serializable@Composable fun NavigationWithParamsExample () { val navController = rememberNavController() NavHost(navController, startDestination = "Home" ) { composable("Home" ) { HomeScreen1 { userId, isFromHome -> val user2 = User2(987654321 , "小明" ) navController.navigate("List5/$user2 " ) } } composable( "List5/{user}" , arguments = listOf( navArgument("user" ) { type = NavType.SerializableType(User2::class .java) }, ) ) { backStackEntry -> val user : User2? = backStackEntry.arguments?.getSerializable("user" ) as User2? user?.run { ListScreen(userId, true ) { id -> navController.navigate("Detail/$id " ) } } } } }
这一点算是目前Compose的短板和缺陷,由于开发者无法在Compose中找到使用传统android传参的方式如Intent/Bundle形式的平替方案,这会使得旧xml项目迁移Compose的成本增大很多,还是希望谷歌能尽快更新给出解决方案吧,不然影响还是很大的。
对象类型传参的其他方案 虽然官方目前没有给出解决方案,但是我们可以采用曲线救国的其他方式,依然可以做到对象方式的传参,这里我大概总结了有以下几种可选的参考方案:
1.使用Gson将数据类序列化成gson字符串传递,然后解析的时候再从字符串反序列化成数据类
2.使用共享的ViewModel实例保存数据类对象(mutableStateOf), 发起方向共享的ViewModel实例中赋值新的数据类对象,接受方从共享的ViewModel实例中读取数据类对象。
3.通过navController.previousBackStackEntry?.savedStateHandle?.set(key, value)/get(key)解决,但是这种有缺点就是跳转之前先弹了回退栈就获取不到了。(所以这种方案只能是在一定条件下可行)
4.使用开源库compose-destinations ,这个库非常棒,使用非常简化(后面会介绍如果使用)
5.使用共享的StateFlow实例,StateFlow是kotlin协程中的Api,基于观察者模式以单向数据管道流的思想编程 (如果不了解的可看我之前的文章 Flow1 Flow2 ),我们页面传参无非就是要在其他页面使用该数据,因此不妨换一种思路,我们进行发送参数,而不是传递参数。
以下是上面第3种方案的实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Parcelize data class User (val userId : Int , val name : String): Parcelable@Composable fun NavigationWithParamsExample () { val navController = rememberNavController() NavHost(navController, startDestination = "Home" ) { composable("Home" ) { HomeScreen1 { userId, isFromHome -> val user = User(56789 , "小明" ) navController.currentBackStackEntry?.savedStateHandle?.set ("user" , user) navController.navigate("List4" ) } } composable( "List4" , ) { backStackEntry -> val user = navController.previousBackStackEntry?.savedStateHandle?.get <User>("user" ) user?.run { ListScreen(userId, true ) { id -> navController.navigate("Detail/$id " ) } } println("user == null is ${user == null} " ) } } }
运行效果: 可以看到传递序列化对象完全没有问题,但是这个方案有一个缺点就是如果在navigate的时候弹了回退栈就不行了,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Parcelize data class User (val userId : Int , val name : String): Parcelable@Composable fun NavigationWithParamsExample () { val navController = rememberNavController() NavHost(navController, startDestination = "Home" ) { composable("Home" ) { HomeScreen1 { userId, isFromHome -> val user = User(56789 , "小明" ) navController.currentBackStackEntry?.savedStateHandle?.set ("user" , user) navController.navigate("List4" ) { popUpTo("Home" ) {inclusive = true } } } } composable( "List4" , ) { backStackEntry -> val user = navController.previousBackStackEntry?.savedStateHandle?.get <User>("user" ) user?.run { ListScreen(userId, true ) { id -> navController.navigate("Detail/$id " ) } } if (user == null ) { with(LocalContext.current) { showToast("user == null" ) } } } } }
运行效果: 可以看到这时接受到的User对象是null,因为这种方案是将User对象保存到当前回退栈中的SavedStateHandle对象中,如果将回退栈清空了,自然就获取不到了。
compose-destinations 库支持对象类型的参数传递。
该库使用kotlin强大的KSP 在编译期进行注解符号处理和生成代码,它的内部只是基于官方Compose的Navigation进行的封装,需要注意的是,compose-destinations 是针对路由导航的通用方案,而并不仅仅是针对传递对象类型的参数,对于任意参数类型传参、以及无参路由跳转都是可以使用的。
集成步骤: 1.在app/build.gradle中添加ksp插件
1 2 3 4 plugins { id 'com.google.devtools.ksp' version '1.7.20-1.0.8' }
ksp插件版本参考:https://github.com/google/ksp/releases,注意它的版本号,是跟你使用的kotlin版本挂钩的。
2.添加compose-destinations的依赖库
1 2 implementation 'io.github.raamcosta.compose-destinations:core:1.7.27-beta' ksp 'io.github.raamcosta.compose-destinations:ksp:1.7.27-beta'
3.设置ksp中间代码保存目录
1 2 3 4 5 6 7 8 9 10 11 android { ... applicationVariants.all { variant -> kotlin.sourceSets { getByName(variant.name) { kotlin.srcDir("build/generated/ksp/${variant.name} /kotlin" ) } } } }
接着就可以在代码中使用了,使用非常简单,首先在需要导航的页面级的Composable上面添加@Destination注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 @RootNavGraph(start = true) @Destination @Composable fun FirstScreen (navigator: DestinationsNavigator ) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text("FirstScreen" , fontSize = 20. sp) Button(onClick = { }) { Text(text = "Go to SecondScreen" ) } } } @Destination @Composable fun SecondScreen ( navigator: DestinationsNavigator , id: Int , name: String ?, isOwnUser: Boolean = false ) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text("SecondScreen" , fontSize = 20. sp) Text("$id $name $isOwnUser " , fontSize = 20. sp) Button(onClick = { }) { Text(text = "Go to ThirdScreen" ) } } } @Destination @Composable fun ThirdScreen ( navigator: DestinationsNavigator , person: Person ) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text("ThirdScreen" , fontSize = 20. sp) Text("$person " , fontSize = 20. sp) } }
这里注意到每个函数上面都有一个 DestinationsNavigator参数,后面生成代码后会使用该参数进行导航,这里暂时不用管只需要添加上即可,然后其他的参数,不管是需要什么类型的,都可以直接按需添加写在函数参数即可。当然如果Composable内部不需要再跳转其他页面,那么函数上就不用添加navigator参数了。
然后build一下项目,就会生成对应的中间代码,添加了@Destination注解的Composable函数就会产生同名且以Destination结尾的类,形如[ComposableName]Destination
然后就可以使用参数navigator.navigate()方法进行跳转,例如这里跳转到SecondScreen,就可以这样写:
1 navigator.navigate(SecondScreenDestination(id = 789 , "王小明" , true ))
类似的,再如跳转到ThirdScreen,注意到ThirdScreen需要接受一个Person对象类型参数,直接传即可:
1 2 val person = Person(1234567 , "Android" )navigator.navigate(ThirdScreenDestination(person))
是不是超级简单,简直比官方的好用一万倍。
完整示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 @Parcelize data class Person (val userId : Int , val name : String): Parcelable@Serializable data class People (val userId : Int , val name : String)data class Man (val userId : Int , val name : String): java.io.Serializable@Composable fun NavigationWithParamsByDestinationsLib () { DestinationsNavHost(navGraph = NavGraphs.root) } @RootNavGraph(start = true) @Destination @Composable fun FirstScreen (navigator: DestinationsNavigator ) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text("FirstScreen" , fontSize = 20. sp) Button(onClick = { navigator.navigate(SecondScreenDestination(id = 789 , "王小明" , true )) }) { Text(text = "Go to SecondScreen" ) } } } @Destination @Composable fun SecondScreen ( navigator: DestinationsNavigator , id: Int , name: String ?, isOwnUser: Boolean = false ) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text("SecondScreen" , fontSize = 20. sp) Text("$id $name $isOwnUser " , fontSize = 20. sp) Button(onClick = { val person = Person(1234567 , "Android" ) navigator.navigate(ThirdScreenDestination(person)) }) { Text(text = "Go to ThirdScreen" ) } } } @Destination @Composable fun ThirdScreen ( navigator: DestinationsNavigator , person: Person ) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text("ThirdScreen" , fontSize = 20. sp) Text("$person " , fontSize = 20. sp) Button(onClick = { val people = People(7654321 , "Kotlin" ) navigator.navigate(FourthScreenDestination(people)) }) { Text(text = "Go to FourthScreen" ) } } } @Destination @Composable fun FourthScreen ( navigator: DestinationsNavigator , people: People ) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text("FourthScreen" , fontSize = 20. sp) Text("$people " , fontSize = 20. sp) Button(onClick = { val man = Man(8866999 , "Compose" ) navigator.navigate(FifthScreenDestination(man)) }) { Text(text = "Go to FifthScreen" ) } } } @Destination @Composable fun FifthScreen ( navigator: DestinationsNavigator , man: Man ) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text("FifthScreen" , fontSize = 20. sp) Text("$man " , fontSize = 20. sp) Button(onClick = { navigator.popBackStack(FirstScreenDestination, inclusive = false ) }) { Text(text = "Back To Home" ) } } }
导航的首页也不需要NavHost那么麻烦的配置了,只需DestinationsNavHost(navGraph = NavGraphs.root)这一句就OK了。
运行效果:
可以看到不管是普通数据类型还是对象类型都可以传递,而且使用方式及其简单,此时如果再回过头去看官方的配置方法,简直又臭又长。
注意:上面示例代码中People数据类使用了@Serializable注解,使用该注解需要参考官网进行配置
路由返回时给上一个页面传值 这里路由返回传参是利用前面对象传参其他方案中的第三种方案提到的 API navController.previousBackStackEntry?.savedStateHandle?.set(key, value)/get(key) 来实现。
例如,有两个屏幕 Screen01 和 Screen02 ,从 Screen01跳转到 Screen02 ,然后从 Screen02 返回 Screen01时给 Screen01传值,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 const val KEY = "my_text" @Composable fun NavigateBackWithResultExample () { val navController = rememberNavController() NavHost(navController = navController, startDestination = "screen01" ) { composable("screen01" ) { entry -> val text = entry.savedStateHandle.get <String>(KEY) Screen01(text) { navController.navigate("screen02" ) } } composable("screen02" ) { Screen02 { result -> navController.previousBackStackEntry?.savedStateHandle?.set (KEY, result) navController.popBackStack() } } } } @Composable fun Screen01 (text: String ?, onNavigateBtnClick: () -> Unit = {}) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(15. dp) ) { Text(text = "当前页面 Screen01" , fontSize = 16. sp) text?.let { Text(text = "来自screen02的结果:$text " , fontSize = 16. sp) } Button(onClick = onNavigateBtnClick) { Text(text = "Go to screen02" ) } } } @Composable fun Screen02 (onNavigateBtnClick: (String ) -> Unit = {}) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(15. dp) ) { var text by remember { mutableStateOf("" ) } Text(text = "当前页面 Screen02" , fontSize = 16. sp) OutlinedTextField( value = text, onValueChange = { text = it }, modifier = Modifier.width(300. dp) ) Button(onClick = { onNavigateBtnClick(text) }) { Text(text = "Go Back" ) } } }
运行效果:
Navigation搭配底部导航栏使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 sealed class Screen (val route: String, val title: String) { object Home : Screen("home" , "Home" ) object Favorite : Screen("favorite" , "Favorite" ) object Profile : Screen("profile" , "Profile" ) object Cart : Screen("cart" , "Cart" ) } val items = listOf( Screen.Home, Screen.Favorite, Screen.Profile, Screen.Cart ) @Composable fun WorkWithBottomNavigationExample () { val navController = rememberNavController() Scaffold( bottomBar = { BottomNavigation { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination items.forEach { screen -> BottomNavigationItem( icon = { Icon(Icons.Filled.Favorite, contentDescription = null ) }, label = { Text(screen.title) }, selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true , onClick = { navController.popBackStack() navController.navigate(screen.route) { popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = true } } ) } } } ) { innerPadding -> NavHost(navController, startDestination = Screen.Home.route, Modifier.padding(innerPadding)) { composable(Screen.Home.route) { HomeScreen2(navController) } composable(Screen.Favorite.route) { FavoriteScreen(navController) } composable(Screen.Profile.route) { ProfileScreen(navController) } composable(Screen.Cart.route) { CartScreen2(navController) } } } }
以上代码有一个需要注意的地方,使用Scaffold中的BottomNavigation 搭配NavHost使用导航时有个问题,如果当前不是在首页(home)Tab页面,而是切换到其他tab页面,那么此时按back键它会先返回到首页(home)Tab页面, 再按一次back键才会退出。
但是一般国内的app效果都是在首页按back键直接回到桌面,不管当前是在哪个tab页,所以上面代码中在onClick方法里调用 navController.navigate方法之前调用了一次navController.popBackStack(),即先弹一次回退栈,否则栈内会保存上次的tab页面。这样就正常了。
多模块下的导航路由配置 当项目采用多模块(Module)组件化 开发方式时,应当在app module中配置Root Graph(因为app依赖编译其他业务模块),将 app module 依赖的其他业务模块的导航配置作为 子Graph,嵌套配置到 NavHost 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Composable fun WorkWithModulesExample () { val navController = rememberNavController() NavHost(navController, startDestination = "home" ) { navigation(startDestination = "MessageList" , route = "home" ) { composable("MessageList" ) { MessageListScreen(navController) } composable("FriendList" ) { FriendListScreen(navController) } composable("Setting" ) { SettingScreen(navController) } } } }
可以将每个模块的路由配置定义为NavGraphBuilder的扩展函数
1 2 3 4 5 6 7 fun NavGraphBuilder.homeGraph (navController: NavController ) { navigation(startDestination = "MessageList" , route = "home" ) { composable("MessageList" ) { MessageListScreen(navController) } composable("FriendList" ) { FriendListScreen(navController) } composable("Setting" ) { SettingScreen(navController) } } }
然后在App module中NavHost里依次调用这些扩展函数
1 2 3 4 5 6 7 8 @Composable fun WorkWithModulesExample2 () { val navController = rememberNavController() NavHost(navController, startDestination = "home" ) { homeGraph(navController) } }
其实多模块下更加适合使用前面提到的开源库compose-destinations 进行路由导航,因为不需要进行大量的配置,app模块会自动依赖其他模块生成的代码。
DeepLink 深度链接 DeepLink 适合的场景:
当前模块跳转到某个业务模块的某个子页面中 ,而不只是该模块的首页面(不管是否多Module还是单Module都存在这种需求)
隐式跳转
DeepLink 是一个标准的URI格式 符合schema://host/path?query 应当在path或之后的部分指定参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 const val URI = "my-app://my.example.app" @Composable fun WorkWithDeepLinkExample () { val navController = rememberNavController() NavHost(navController, startDestination = "SomeModule" ) { composable( route = "newsDetail?id={id}" , deepLinks = listOf( navDeepLink { uriPattern = "$URI /news/{id}" action = Intent.ACTION_VIEW } ) ) { backStackEntry -> NewsDetailScreen(navController, backStackEntry.arguments?.getString("id" )) } composable("SomeModule" ) { SomeModuleScreen { val request = NavDeepLinkRequest.Builder .fromUri("$URI /news/1234" .toUri()) .build() navController.navigate(request) } } } } @Composable fun NewsDetailScreen (navController : NavController , newsId : String ?) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text("NewsDetailScreen $newsId " , fontSize = 20. sp) } } @Composable fun SomeModuleScreen (onNavigate : () -> Unit ) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Button(onClick = onNavigate) { Text(text = "跳转到NewsDetailScreen" ) } } }
借助这些深层链接,可以将特定的网址、操作或 MIME 类型与可组合项关联起来。 默认情况下,这些深层链接不会向外部应用公开。如需向外部提供这些深层链接,必须向应用的 manifest.xml 文件添加相应的 <intent-filter> 元素。在清单的 <activity> 元素中添加以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 <activity …> <intent-filter > <action android:name ="android.intent.action.MAIN" /> <category android:name ="android.intent.category.LAUNCHER" /> </intent-filter > <intent-filter > <action android:name ="android.intent.action.VIEW" /> <category android:name ="android.intent.category.DEFAULT" /> <category android:name ="android.intent.category.BROWSABLE" /> <data android:scheme ="my-app" android:host ="my.example.app" /> // 这里要跟定义的URI对应上 </intent-filter > </activity >
对外声明URI以后,就可以跨进程打开页面了,可以通过adb命令进行测试:
1 adb shell am start -d "my-app://my.example.app/news/1234" -a android.intent.action.VIEW
还可以通过URI构建PendingIntent, 在通知栏消息通知等场景中点击打开应用中的Compose页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 val id = "1234" val context = LocalContext.currentval deepLinkIntent = Intent( Intent.ACTION_VIEW, "my-app://my.example.app/news/$id " .toUri(), context, MyActivity::class .java ) val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run { addNextIntentWithParentStack(deepLinkIntent) getPendingIntent(0 , PendingIntent.FLAG_UPDATE_CURRENT) }
另外,前面提到的compose-destinations 导航库也支持DeepLink的使方式,具体可以查看:deeplinks
Navigation对ViewModel的支持 viewModel() 是androidx-lifecycle针对Compose提供的Composable方法,它通过 LocalViewModelStoreOwner.current 获取最近的 ViewModelStoreOwner ,可能是Activity或Fragment, 在一个由 Composable 组成的单 Activity 应用中,相当于所有ViewModel都放在一起,所有的Compose页面共享ViewModel实例。
有时我们希望为每一个页面的Composable单独提供一个ViewModel实例,Navigation更容易做到这一点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class ExampleViewModule : ViewModel () { var _name = mutableStateOf("" ) val name = _name } @Composable fun WorkWithViewModelExample () { val navController = rememberNavController() NavHost(navController, startDestination = "example" ) { composable("example" ) { backStackEntry -> val exampleViewModel = viewModel<ExampleViewModel>() SomeScreen(exampleViewModel) } } } @Composable fun SomeScreen (viewModel: ExampleViewModel = viewModel() ) {}
每个 backStackEntry 都是一个 ViewModelStoreOwner,所以当前viewModel()函数创建的ViewModel单例只服务于当前页面,随着页面从回退栈中弹出,ViewModelStore被清空,所辖的ViewModel会执行onClear操作。
从 Compose 导航到其他 Fragment 页面 使用基于 fragment 的 Navigation 从 Compose 导航,要在 Compose 代码内更改目的地,可以公开传递由层次结构中的任何可组合项触发的事件:
1 2 3 4 @Composable fun MyScreen (onNavigate: (Int ) -> ()) { Button(onClick = { onNavigate(R.id.nav_profile) } { } }
在 fragment 中,可以通过找到 NavController 实例并导航到目的地,在 Compose 和基于 fragment 的 Navigation 组件之间架起桥梁:
1 2 3 4 5 override fun onCreateView ( ) { setContent { MyScreen(onNavigate = { dest -> findNavController().navigate(dest) }) } }
或者,可以将 NavController 传递到 Compose 层次结构下方。不过,公开简单的函数的可重用性和可测试性更高。
如果 Fragment 没有使用 Navigation 组件库,那么只能在Compose公开的回调函数中使用FragmentManager 进行跳转了(Compose属于当前的Fragment 中的View)。
从 Compose 导航到其他 Activity 页面 从 Compose 跳转到其他 Activity 页面就是启动Activity的代码,其实跟导航组件没有多大关系了,我们可以在Composable暴露出的点击事件函数中进行跳转:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Composable fun NavigationExample2 () { val navController = rememberNavController() NavHost(navController = navController, startDestination = "Welcome" ) { composable("Welcome" ) { val context = LocalContext.current WelcomeScreen { val intent = Intent(context, OtherActivity::class .java).apply { putExtra("name" , "张三" ) putExtra("uid" , 123 ) } context.startActivity(intent) } } } } @Composable fun WelcomeScreen (onClick: () -> Unit = {}) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text("WelcomeScreen" , fontSize = 20. sp) Button(onClick = onClick) { Text(text = "Go to Other" ) } } }
如果是以startForResult的方式启动,最好是通过带回调接口的方式去启动,这样在回调接口中直接获取返回结果进行展示,否则只有在Composable所属的Activity的onActivityResult中处理再通过顶层组件传入,比较麻烦。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Composable fun NavigationExample2 () { val navController = rememberNavController() NavHost(navController = navController, startDestination = "Welcome" ) { composable("Welcome" ) { val context = LocalContext.current var resultText by remember { mutableStateOf("" ) } WelcomeScreen(resultText) { val intent = Intent(context, OtherActivity::class .java).apply { putExtra("name" , "张三" ) putExtra("uid" , 123 ) } if (context is Activity) { ActivityStarter.startForResult(context, intent, object : ActivityResultListener { override fun onSuccess (result: Result ?) { val name = result?.data ?.getStringExtra("name" ) val uid = result?.data ?.getIntExtra("uid" , -1 ) resultText = "name: $name uid: $uid " } override fun onFailed (result: Result ?) { } }) } } } } } @Composable fun WelcomeScreen (result: String , onClick: () -> Unit = {}) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text("WelcomeScreen result: $result " , fontSize = 20. sp) Button(onClick = onClick) { Text(text = "Go to Other" ) } } }
另一种方式是当前Composable只需要监听ViewModel中的mutableStateOf的状态值或者监听StateFlow,而在onActivityResult中更新ViewModel或者StateFlow中的值,那么使用该值的Composable就会自动重组刷新。
更多关于 startForResult 方式启动Activity的内容请查看Jetpack Compose中的startActivityForResult的正确姿势
参考资料: