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代码中进行控制,更加方便灵活了。
导航路由配置
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, isFromHome) { id ->
navController.navigate("Detail/$id")
}
}
设置可选参数时,接受方必须提供默认值参数配置。
对象类型的传参
对于数据类或普通class对象类型的参数传递,首先想到的是传递序列化对象,但是很遗憾,官方目前还不支持对象类型的参数传递,虽然如此,但是很奇怪的是,你可以通过代码写出序列化的传参方式,例如以下通过Parcelable
序列化的方式传参:
@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") // NOT SUPPORTED!!!
}
}
// NOT SUPPORTED!!! navigation-compose暂不支持直接传Parcelable
composable(
"List3/{user}", // 传递Parcelable数据类
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
序列化方式的传参也会崩溃,会报同样的错误
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") // NOT SUPPORTED!!!
}
}
// NOT SUPPORTED!!! navigation-compose暂不支持直接传Serializable
composable(
"List5/{user}", // 传递Serializable数据类
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种方案的实现代码:
@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
的时候弹了回退栈就不行了,例如:
@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进行路由导航
compose-destinations库支持对象类型的参数传递。
该库使用kotlin强大的KSP在编译期进行注解符号处理和生成代码,它的内部只是基于官方Compose的Navigation
进行的封装,需要注意的是,compose-destinations是针对路由导航的通用方案,而并不仅仅是针对传递对象类型的参数,对于任意参数类型传参、以及无参路由跳转都是可以使用的。
集成步骤:
1.在app/build.gradle中添加ksp插件
plugins {
// ...
id 'com.google.devtools.ksp' version '1.7.20-1.0.8'
}
ksp插件版本参考:https://github.com/google/ksp/releases,注意它的版本号,是跟你使用的kotlin版本挂钩的。
2.添加compose-destinations的依赖库
implementation 'io.github.raamcosta.compose-destinations:core:1.7.27-beta'
ksp 'io.github.raamcosta.compose-destinations:ksp:1.7.27-beta'
3.设置ksp中间代码保存目录
android {
...
// replace applicationVariants with libraryVariants if the module uses 'com.android.library' plugin!
applicationVariants.all { variant ->
kotlin.sourceSets {
getByName(variant.name) {
kotlin.srcDir("build/generated/ksp/${variant.name}/kotlin")
}
}
}
}
接着就可以在代码中使用了,使用非常简单,首先在需要导航的页面级的Composable
上面添加@Destination
注解:
@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 = {
// TODO
}) {
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 = {
// TODO
}) {
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
参数,后面生成代码后会使用该参数进行导航,这里暂时不用管只需要添加上即可,然后其他的参数,不管是需要什么类型的,都可以直接提供写在函数参数即可。
然后build
一下项目,就会生成对应的中间代码,添加了@Destination
注解的Composable函数就会产生同名且以Destination结尾的类,形如[ComposableName]Destination
然后就可以使用参数navigator.navigate()
方法进行跳转,例如这里跳转到SecondScreen
,就可以这样写:
navigator.navigate(SecondScreenDestination(id = 789, "王小明", true)) // 传递基本数据类型参数
类似的,再如跳转到ThirdScreen
,注意到ThirdScreen
需要接受一个Person
对象类型参数,直接传即可:
val person = Person(1234567, "Android")
navigator.navigate(ThirdScreenDestination(person)) // 传递对象类型参数
是不是超级简单,简直比官方的好用一万倍。
完整示例代码:
@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
注解,使用该注解需要参考官网进行配置
Navigation搭配底部导航栏使用
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 {
// 从 NavHost 函数中获取 navController 状态,并与 BottomNavigation 组件共享此状态。
// 这意味着 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 = {
// 加这个可解决问题:按back键会返回2次,第一次先返回home, 第二次才会退出
navController.popBackStack()
navController.navigate(screen.route) {
// 点击item时,清空栈内 popUpTo ID到栈顶之间的所有节点,避免站内节点持续增加
popUpTo(navController.graph.findStartDestination().id) {
saveState = true // 用于页面状态的恢复
}
// 避免多次重复点击按钮时产生多个实例
launchSingleTop = true
// 再次点击之前选中的Item时,恢复之前的状态
restoreState = true
// 通过使用 saveState 和 restoreState 标志,当您在底部导航项之间切换时,
// 系统会正确保存并恢复该项的状态和返回堆栈。
}
}
)
}
}
}
) { 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
中。
@Composable
fun WorkWithModulesExample() {
val navController = rememberNavController()
NavHost(navController, startDestination = "home") {
//...
// 当调用 navigate('home') 时,会自动将home模块的MessageList作为页面显示
navigation(startDestination = "MessageList", route = "home") {
composable("MessageList") { MessageListScreen(navController) }
composable("FriendList") { FriendListScreen(navController) }
composable("Setting") { SettingScreen(navController) }
}
//...其他模块的设置,每个模块对应一个navigation子项
}
}
可以将每个模块的路由配置定义为NavGraphBuilder
的扩展函数
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里依次调用这些扩展函数
@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
或之后的部分指定参数。
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}" // 对应上面route的深度链接
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>
元素中添加以下内容:
<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
命令进行测试:
adb shell am start -d "my-app://my.example.app/news/1234" -a android.intent.action.VIEW
还可以通过URI
构建PendingIntent
, 在通知栏消息通知等场景中点击打开应用中的Compose
页面:
val id = "1234"
val context = LocalContext.current
val 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)
}
Navigation对ViewModel的支持
viewModel()
是androidx-lifecycle
针对Compose
提供的Composable
方法,它通过 LocalViewModelStoreOwner.current
获取最近的 ViewModelStoreOwner
,可能是Activity
或Fragment
, 在一个由 Composable
组成的单 Activity
应用中,相当于所有ViewModel
都放在一起,所有的Compose
页面共享ViewModel
实例。
有时我们希望为每一个页面的Composable
单独提供一个ViewModel
实例,Navigation
更容易做到这一点
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
代码内更改目的地,可以公开传递由层次结构中的任何可组合项触发的事件:
@Composable
fun MyScreen(onNavigate: (Int) -> ()) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
在 fragment
中,可以通过找到 NavController
实例并导航到目的地,在 Compose
和基于 fragment
的 Navigation
组件之间架起桥梁:
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暴露出的点击事件函数中进行跳转:
@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
中处理再通过顶层组件传入,比较麻烦。
@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) {
// 以回调方式启动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
就会自动重组刷新。
参考资料:
- Jetpack Compose navigation
- 《Jetpack Compose从入门到实战》- 机械工业出版社 - 2022年9月