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的操作。
navController.navigate("Home"){
popUpTo("Welcome")
}
navController.navigate("Home"){
popUpTo("Welcome"){ inclusive = true }
}
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")
接受方:
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")
}
}
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 序列化方式的传参也会崩溃,会报同样的错误
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种方案的实现代码:
@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库支持对象类型的参数传递。
该库使用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 {
...
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 = {
}) {
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 参数,后面生成代码后会使用该参数进行导航,这里暂时不用管只需要添加上即可,然后其他的参数,不管是需要什么类型的,都可以直接提供写在函数参数即可。
然后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 {
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 中。
@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 的扩展函数
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}"
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) {
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 就会自动重组刷新。
参考资料:
|