当前位置: 首页 > news >正文

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代码中进行控制,更加方便灵活了。

导航路由配置

NavControllerNavigation 的核心,它是有状态的,可以跟踪返回堆栈以及每个界面的状态。可以通过 rememberNavController 来创建一个NavController的实例。

NavHost 是导航容器,NavHostNavController 与导航图相关联,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 ,可能是ActivityFragment, 在一个由 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 页面

使用基于 fragmentNavigationCompose 导航,要在 Compose 代码内更改目的地,可以公开传递由层次结构中的任何可组合项触发的事件:

 @Composable
 fun MyScreen(onNavigate: (Int) -> ()) {
     Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
 }

fragment 中,可以通过找到 NavController 实例并导航到目的地,在 Compose 和基于 fragmentNavigation 组件之间架起桥梁:

 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月

相关文章:

  • 如何做网络网站推广/产品seo是什么意思
  • 时时彩网站建设/搜索引擎营销例子
  • 如何自建网站接广告/中国企业网
  • 杭州手机模板建站/google高级搜索
  • 网站为什么上传不了图片/最新国际新闻事件今天
  • 百度怎么搜索到自己的网站/中国站长之家
  • Java项目:springboot课程自动排课系统
  • 启动项目端口被占用无奈只能重启?程序员的电脑绝不允许重启!
  • Huffman二进制编码以及文本的压缩与解压
  • 单商户商城系统功能拆解51—应用中心—评价助手
  • 基于注解方式实现Spring Security忽略拦截
  • Leetcode 1792. 最大平均通过率 题解
  • k8s之多方面多维度的资源隔离和限制(namespace,LimitRange,ResourceQuota)
  • Himall商城更新插件列表\获取插件程序集文件\ 深复制IPlugin
  • 【Web前端HTML5CSS3】09、高度塌陷与 BFC
  • Java中toString方法的推荐实现方式
  • 构建系列之新一代利器Esbuild(上)
  • MIUI10国际版系统自定义字体设置办法