1.Navigation
Compose
本身缺乏类似Fragment
的页面导航功能,而Navigation
库为此提供了关键支持。 它既像一个粘合剂,将高内聚的页面以低耦合的方式组织起来;又像一个管家,负责管理导航页面的状态和对应的回退栈。
2.回退栈
- Navigation组件内部维护一个目的地堆栈,堆栈的底部为起始目的地,导航到一个新的目的地时会将该目的地推送到堆栈的顶部,这样便形成了一个回退栈。
- 回退栈里的元素类型是
NavBackStackEntry
,它不仅存储导航中携带的参数,也是代表了这个目的地的生命周期状态。 - 页面导航的过程中,提供了操作栈内元素的方法,比如哪些元素出栈之后,再压入新的元素到栈顶。这有点类似于
Activity
的任务栈。 - 当点击系统自带的NavigationBar的返回按钮时,堆栈里的目的地就会依次弹出,最后会显示初始目的地,再次点击返回按钮时,退出应用。
3.Navigation组件
Navigation主要包含以下几个组件:
3.1 宿主 NavHost
包含导航目的地的界面元素,当用户浏览应用时,会在导航宿主中切换目的地。 NavHost在构建的时候,会构建出NavGraph
。
3.2 导航图 NavGraph
一种数据和行为的关系图,定义应用中的所有导航目的地以及它们如何连接在一起,是导航的蓝图。
3.3 控制器 NavController
管理目的地之间导航的状态、处理深层链接、管理返回栈等。
3.4 目的地 NavDestination
导航图中的节点数据,代表应用中一个内容屏幕,当用户导航到此节点时,会显示此目的地的内容。
3.5 路线 route
任何可序列化的数据类型,唯一标识目的地及其所需的任何数据。使用路线导航,并前往目的地。
4. 使用Navigation
4.1 创建导航控制器 NavController
在使用 Jetpack Compose 时创建 NavController,请调用 rememberNavController()
:
kotlin
val navController = rememberNavController()
一般是在顶级的Composable函数中创建NavController,这样所有组件才能引用它。
4.2 创建导航图 NavGraph
在Compose中,使用NavHost可组合项,在构建NavHost的过程中,就直接构建了导航图,这是推荐的方式。 说白了,就是在NavHost的lambda
表达式里,直接定义每个页面的标签,以及对应的跳转逻辑,如下:
kotlin
NavHost(navController = navController, "ChatList") {
composable (route = "ChatList"){
ChatListPage {
navController.navigate("ChatDetail")
}
}
composable(route = "ChatDetail") {
ChatDetailPage()
}
}
通过compoasable函数去定义了两个页面,一个是聊天列表页ChatListPage()
,一个是具体某个人的聊天详情页*ChatDetailPage()
。在ChatListPage后面跟了一个Lambda表达式,在ChatListPage内部的列表item点击的时候,会调用这个lambda表达式,并导航到ChatDetailPage页面。
route
参数就是这个页面唯一的标识,也叫做路线,使用可序列化对象或者类定义路线。我这里使用的是字符串(可序列化)作为路线,优点是实现简单,但不太利于传参。
我们做一下修改,使用可序列化的 类和对象
定义路线:
kotlin
@Serializable
object ChatListRoute
@Serializable
data class ChatDetailRoute(val name : String) //这里定义一个参数,在导航跳转的时候传递
这里要注意,实现序列化需要引入Kotlin序列化插件(Kotlin serialization plugin)
,以及json序列化库依赖(JSON serialization library)
,如下:
kotlin
[versions]
kotlin = "2.2.10"
navigation = "2.9.4"
serialization = "1.9.0"
[libraries]
navigation = {group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation"}
serialization = {group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization"}
[plugins]
kotlin-serialization = {id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
在Activity中构建NavGraph如下:
kotlin
NavHost(navController = navController, ChatListRoute) {
composable<ChatListRoute> {
ChatListPage {
navController.navigate(ChatDetailRoute(it)) // 注释1
}
}
composable<ChatDetailRoute>{
ChatDetailPage(it.toRoute<ChatDetailRoute>().name) //注释2
}
}
在代码注释1
处,通过 lambda表达式传出的数据给到ChatDetailRoute,这里的it是点击的对话列表Item对应的联系人名字。当导航跳转到对应目的地时,这个参数也会带过去。
在代码注释2
处,导航跳转到目的地之后,就把ChatDetailRoute携带的数据取出来,给到目的地页面显示。每当用户导航到一个新的目的地 (Destination) 时,Navigation 组件会在后退栈的顶部创建一个新的 NavBackStackEntry 实例来表示这个刚刚访问的目标。
4.3 导航触发
我们通过naviController的navigation
方法触发导航,参数是目的地页面的路线。 默认情况下,navigation的行为是在回退栈压入一个新的Composable的Destination,然后作为栈顶节点进行显示。但navigation方法也提供了一个lambda,可以追加NavOptions
的操作。
- 比如,清空栈顶节点到HomeRoute之间的所有节点(
不包含HomeRoute
),然后 再入栈ChatDetailRoute。当从聊天详情页返回的时候,直接回到主页。可如下实现:
kotlin
navController.navigate(ChatDetailRoute(it)){
popUpTo(HomeRoute)
}
- 再看看,清空栈顶节点到WelcomeRoute之间的所有节点(
包含WelcomeRoute
),然后 再入栈HomeRoute。通过欢迎页进入到主页之后,从主页再回退时,不需要再进入欢迎页,直接退出应用。下面的代码:
kotlin
navController.navigate(HomeRoute){
popUpTo(WelcomeRoute){ inclusive = true}
}
- 当栈顶节点已经是Home页面时,不会重新入栈新的Home节点,相当于Activity的
SingleTop
的launchMode,如下:
kotlin
navController.navigate(HomeRoute){
launchSingleTop = true
}
4.4 页面内容
我们看看ChatListPage最简单的内容代码:
kotlin
@Composable
fun ChatListPage(onclick : (String) -> Unit) {
LazyColumn{
itemsIndexed(items = chatlists) { index, chat ->
...
ChatItem(chat, Modifier.clickable{onclick(chat)})
}
}
}
@Composable
fun ChatItem(name : String, modifier: Modifier) {
Text(name, modifier
.fillMaxSize()
.background(Color.White))
}
这里最重要的就是把点击事件处理的逻辑,通过高阶函数的方式抽象出来,并把具体的处理逻辑交给调用者实现。这是一种典型的"状态上提"
策略。
5. 嵌套导航 Nested Navigation Graph
大型的项目会以组件化的方式开发,会涉及多个Module
模块,每个模块相互独立,而且每个模块中也有自己的导航逻辑,即也存在一个NavGraph。
当App Module依赖各个Lib Module的时候,可以借助Nested Navitation Graph
机制,App Moddule中定义root Graph
,而各个模块可以作为其子 Graph
存在并关联。
以账号登陆逻辑为例: 大多数应用都有一个用户认证流程,可能包含登录、注册、忘记密码、手机验证等多个步骤。这个流程通常是自包含的,用户完成或取消后会返回到应用的主流程。
使用嵌套导航图的优点:
- 封装性: 登录/注册相关的屏幕(如登录页、注册页、验证码页)可以被组织在一个单独的嵌套图中。主导航图只需要知道如何导航到这个"登录流程图",而不需要关心其内部的具体页面。
- 可重用性: 如果应用的多个地方都需要触发登录(例如,在未登录时尝试访问受限功能),可以直接导航到这个嵌套的登录流程图。
- 可读性: 主导航图会变得更简洁,只需指向"登录流程"子导航图的入口,保持了主导航图的整体架构。
如下是实现的部分代码示例:
kotlin
object Routes {
const val HOME = "home"
const val PROFILE = "profile"
// Auth Flow related
const val AUTH_GRAPH_ROUTE = "auth_graph" // 路由名给整个登录流程图
const val LOGIN = "login"
const val REGISTER = "register"
}
在登陆模块的内部实现一个NavGraphBuilder的扩展函数,目的是把内部的导航图内聚,并方便地暴露给主导航调用:
kotlin
fun NavGraphBuilder.authGraph(navController: NavHostController) {
navigation(
route = Routes.AUTH_GRAPH_ROUTE, // 整个嵌套图的路由
startDestination = Routes.LOGIN // 这个嵌套图的起始页面
) {
composable(Routes.LOGIN) {
LoginScreen(
onLoginSuccess = {
// 登录成功后,返回到之前的屏幕或导航到主页
// 通常会清除登录流程的后退栈
navController.popBackStack(Routes.AUTH_GRAPH_ROUTE, inclusive = true) // 清除整个登录流程
},
onNavigateToRegister = {
navController.navigate(Routes.REGISTER)
}
)
}
composable(Routes.REGISTER) {
RegisterScreen(
onRegisterSuccess = {
navController.popBackStack(Routes.AUTH_GRAPH_ROUTE, inclusive = true)// 清除整个登录流程
},
onNavigateBackToLogin = {
navController.popBackStack() // 返回到登录页
}
)
}
// 可以添加更多页面,如忘记密码等
}
}
主导航图:
kotlin
NavHost(navController = navController, startDestination = Routes.HOME) {
composable(Routes.HOME) {
HomeScreen(
onNavigateToProfile = {
if (UserSession.isLoggedIn.value) {
navController.navigate(Routes.PROFILE)
} else {
// 如果未登录,导航到整个登录流程图
navController.navigate(Routes.AUTH_GRAPH_ROUTE)
}
}
)
}
composable(Routes.PROFILE) {
......
// 如果有判断未登录,导航到整个登录流程图
navController.navigate(Routes.AUTH_GRAPH_ROUTE)
}
// 在这里集成登录流程的嵌套图
authGraph(navController)
}
6. Navigation和HorizontalPager
-
Navigation: 主要用于管理应用中不同屏幕或目的地 (Destinations) 之间的切换。它构建了一个导航图 (Navigation Graph),定义了各个屏幕以及它们之间的导航路径。
它主要关注的是应用的整体导航结构,如从列表页跳转到详情页,从设置页返回主页等。也负责处理回退栈、参数传递、深层链接 、嵌套导航图等。
-
HorizontalPager : 用于实现内容的分页展示,允许用户通过水平或垂直滑动来切换不同的页面 ,比如常见的
标签页 (Tabs)
、轮播图 (Banner)
、引导页
等。它主要关注在
同一个屏幕区域
内,通过手势滑动来展示一系列相关联的内容片段。
通常,包含HorizontalPager的Composable函数会作为一个导航图中的目的地(NavDestination)。比如我们熟知的微信,如下:
它的主页HomePage就是一个典型的标签页,内部包含了一个HorizontalPager,它内部又包含TabItem:聊天
、通讯录
、发现
、我
,这4个pager页面,通过滑动来切换。 HorizontalPager内部的页面内容,通常也是可以点击的,并导航到其他目的地。比如点击聊天里面的某个聊天项Item
,就会跳转到聊天详情页。