全面解析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,基于观察者模式以单向数据管道流的思想编程
我们页面传参无非就是要在其他页面使用该数据,因此不妨换一种思路,我们进行发送参数,而不是传递参数。以下是上面第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 参数,后面生成代码后会使用该参数进行导航,这里暂时不用管只需要添加上即可,然后其他的参数,不管是需要什么类型的,都可以直接按需添加写在函数参数即可。当然如果 Composable 内部不需要再跳转其他页面,那么函数上就不用添加 navigator 参数了。
然后 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 注解,使用该注解需要参考官网进行配置。
路由返回时给上一个页面传值
这里路由返回传参是利用前面对象传参其他方案中的第三种方案提到的 API navController.previousBackStackEntry?.savedStateHandle?.set(key, value)/get(key) 来实现。例如,有两个屏幕 Screen01 和 Screen02 ,从 Screen01跳转到 Screen02 ,然后从 Screen02 返回 Screen01时给 Screen01传值,代码如下:
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) // 尝试从NavBackStackEntry中获取值
Screen01(text) {
navController.navigate(“screen02”)
}
}
composable(“screen02”) {
Screen02 { result ->
navController.previousBackStackEntry?.savedStateHandle?.set(KEY, result) // 向前一个Entry回传结果
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搭配底部导航栏使用   /
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 文件添加相应的元素。在清单的元素中添加以下内容:
 <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)
}

另外,前面提到的 compose-destinations 导航库也支持 DeepLink 的使方式。
/   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 就会自动重组刷新。
川峰的博客地址:https://blog.csdn.net/lyabc123456?type=blog
RIPRO主题是一个优秀的主题,极致后台体验,无插件,集成会员系统
源码星空 » 全面解析Jetpack Compose中的导航路由–[引用大佬川峰之作]

发表评论

提供最优质的资源集合

立即查看 了解详情