在上篇文章 Compose-Navigation3-为什么会有Navigation3 中,咱们介绍了为什么会有 Navigation3 这么一个导航框架,简单来说就是在现代数据驱动页面的声明式 UI 框架的趋势下,单页面应用(SPA)已经渐渐成为一种主流,Navigation3 就是为了适应 SPA 而生的。
再次强调 Navigation3 的作用: 管理场景跳转以及组合显示页面为场景。
这篇文章以具体的案例讲一讲 Navigation3 的基础用法,基础用法重点在页面 (场景)跳转,我们以页面跳转为切入点。写一个简单的 Demo,功能如下:
- 包含三个页面:欢迎页面、菜单页面和菜品详情页面。
- 用户进入 APP,进入欢迎页面,2 秒后自动进入菜单页面
- 用户点击菜品,进入菜品详情页面。
- 滑动返回或点击返回按钮返回主菜单页面。
页面如下:
通过这么一个简单的 Demo,咱们来盘一盘 Navigation3 的基础用法:
- 如何组合多个页面
- 页面间如何传递参数
- 如何通过控制页面返回栈控制页面跳转
页面返回栈
Navigation3 使用页面返回栈来组织页面显示,要想理解 Navigation3,首先得搞懂页面返回栈是个啥。得从栈这个数据结构说起:
栈这种数据结构,先入后出,或者说后入先出。啥意思呢?一个栈可以理解为羽毛球筒,最先放进羽毛球桶的羽毛球,最后才能拿出来,最后放进羽毛球桶的羽毛球最先拿出来。这是不是就好理解了呢?
那么为什么要使用栈这种数据结构呢?
我们先来看看一个页面导航要考虑那些东西:
- 一个页面导航要决定哪个页面显示在最上层。
- 一个页面导航要记录页面跳转历史,就好像是浏览器的浏览历史一样,点击返回按钮的时候,能够返回上一个页面。
使用栈这种数据结构,可以很容易实现上面的逻辑
- 栈顶的内容(羽毛球桶最上面的羽毛球)就是当前可以看到的页面(下面的被遮住了肯定看不到)
- 退出当前页面的时候,相当于把羽毛球桶最上面的羽毛球拿了出来,就能看到被压在下面的羽毛球了,被压在下面的羽毛球就是页面被加到里面的顺序,也就能保证记录页面的跳转历史了。
这也就是页面返回栈的原理了,不单单是 Navigation3 中使用了这种数据结构,最开始的 Activity 栈也是这种模式,包括浏览器的返回功能底层使用的也是栈数据结构。
Navigation3 中的页面
Navigation3 同样用栈来组织页面,但具体管理方式有自己的一套玩法。咱们先把几个核心概念整明白:
- NavKey:页面的唯一标识,不同的 Key 绑定不同页面。框架通过识别栈里的 Key,决定当前该显示哪个页面
- NavBackStack:页面返回栈本身,栈里装的都是 NavKey
- NavEntry:每个 Entry 包含一个 NavKey 和对应的 Compose 函数。框架通过 Key 找到对应的函数,渲染出页面
- NavDisplay:Navigation3 的核心组件,导航的所有定义都在这儿完成
添加依赖
libs.versions.toml
[versions]
lifecycleViewmodelNav3 = "2.10.0"
nav3Core = "1.0.0"
kotlin = "2.3.10"
[libraries]
androidx-navigation3-runtime = { group = "androidx.navigation3", name = "navigation3-runtime", version.ref = "nav3Core" }
androidx-navigation3-ui = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "nav3Core" }
androidx-lifecycle-viewmodel-navigation3 = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
[plugins]
# 添加 Kotlin 序列化插件, NavKey 需要使用序列化插件来序列化
jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
build.gradle.kts
plugins {
// 添加 Kotlin 序列化插件, NavKey 需要使用序列化插件来序列化
alias(libs.plugins.jetbrains.kotlin.serialization)
}
dependencies {
implementation(libs.l.androidx.navigation3.runtime)
implementation(libs.l.androidx.navigation3.ui)
implementation(libs.l.androidx.lifecycle.viewmodel.navigation3)
}
Nav 3 中的返回栈
Navigation3 和 Navigation2 的一个重要区别在于 —— Navigation3 不通过 NavController 来间接管理导航状态,而是直接将页面返回栈的控制权交给开发者,让开发者可以更灵活地操作栈。简单来说:
- 往栈里加个 NavKey,就相当于打开一个新页面
- 从栈里移除个 Key,就相当于关闭当前页面
但是这个栈还不是简单地栈,它允许你像一个列表一样进行操作,所以给了我们很大的灵活度。
创建导航很简单,首先创建一个页面返回栈,再使用 NavDisplay 组件来配置导航行为。
// 创建一个页面返回栈
val navBackStack = rememberNavBackStack()
NavDisplay(
// 将页面返回栈传递给 NavDisplay 组件
backStack = navBackStack,
// 我们希望监听返回事件,使用 onBack 来监听
// 其实默认的逻辑和下面的逻辑一致,这里写出来只是为了演示
onBack = {
// 在返回时,移除栈顶的页面,也就是显示上一个页面
navBackStack.removeLastOrNull()
},
)
Nav 3 中的页面
现在咱们已经创建了 NavDisplay 组件,但它还只是个空壳子,因为还没往里面加页面。
前面说过,页面由 NavEntry 表示,每个 Entry 都包含一个 NavKey 和对应的 Compose 函数。框架就靠识别 NavKey 来决定当前该显示哪个页面。
咱们的 Demo 需要三个页面,所以得创建三个对应的 NavKey:
- 欢迎页面:不需要参数,就是个简单的标识,用
data object来表示就行 - 菜单页面:同样不需要参数,也是个标识,还是用
data object - 菜品详情页面:需要根据菜品 ID 来显示不同内容,所以得用
data class来承载这个参数
/**
* 欢迎页面Key
*/
@Serializable
data object Welcome : NavKey
/**
* 主页Key
*/
@Serializable
data object Home : NavKey
/**
* 菜品详情
*/
@Serializable
data class Detail(val id: Int) : NavKey
对此我们需要创建三个页面组件,分别对应欢迎页面、菜单页面和菜品详情页面。页面内容非重点,就不再详细描述了,只简单描述页面参数和功能,具体页面源码可以在 ComposeSample 这里找到。
/**
* 欢迎页面
*
* 该页面启动后,2秒后自动跳转,跳转时调用 onTimeout */
@Composable
fun WelcomePage(onTimeout: () -> Unit = {}) {}
/**
* 菜品列表页面
* @param onDishSelect 菜品选择回调
*/
@Composable
fun DishListPage(onDishSelect: (Int) -> Unit = {}) {}
/**
* 菜品详情页面
* @param id 菜品Id
*/
@Composable
fun DishDetailPage(id: Int = 1) {}
现在问题来了:怎么把这些页面组织到一起呢?
NavDisplay 提供了一个 entryProvider 参数。通过它,我们可以定义不同的 NavKey 对应哪个 Compose 函数。
同时,咱们还得把页面跳转的逻辑理清楚:
- 初始状态:应用启动时,应该显示欢迎页面。所以导航栈初始化时,里面只放一个
WelcomeKey - 欢迎页跳转:欢迎页显示2秒后,自动跳转到菜单页。这时候需要把
HomeKey 加到导航栈里 - 清理欢迎页:跳转后,得把
WelcomeKey 从栈里移除, 不然用户在主页点返回,又会回到欢迎页,这体验就太拉胯了 - 点击菜品:用户在菜单页点击某个菜品,就把对应的
DetailKey 加到栈里,显示详情页 - 返回操作:用户在详情页滑动返回,就把
DetailKey 从栈里移除,回到菜单页
// 创建导航栈,初始时只放一个 Welcome Key,这样应用启动就显示欢迎页
val navBackStack = rememberNavBackStack(Welcome)
NavDisplay(
// 将页面返回栈传递给 NavDisplay 组件
backStack = navBackStack,
modifier = Modifier.fillMaxSize(),
// 我们希望监听返回事件,使用 onBack 来监听
// 其实默认的逻辑和下面的逻辑一致,这里写出来只是为了介绍
onBack = {
// 在返回时,移除栈顶的页面,也就是显示上一个页面
navBackStack.removeLastOrNull()
},
// 定义不同 NavKey 对应的页面
entryProvider = entryProvider {
// 欢迎页面
entry<Welcome> {
WelcomePage {
// 先移除掉欢迎页面,因为跳转后,欢迎页面将不再显示,不能点了返回之后又跳回到欢迎页面了
navBackStack.removeIf { it is Welcome }
// 将主页面添加到导航栈中
navBackStack.add(Home)
}
}
// 主页
entry<Home> {
DishListPage(onDishSelect = {
// 点击菜品后,将菜品详情页面添加到导航栈中,显示该菜品详情页面
navBackStack.add(Detail(it))
})
}
// 菜品详情页
entry<Detail> { detail ->
DishDetailPage(detail.id)
}
}
)
后续
以上就是 Navigation3 的基础用法,其实用羽毛球桶来描述页面栈只是一个简单的类比,实际使用中,这种基础用法用羽毛球桶来描述非常合适,就是一个页面占用一个完整屏幕,但是实际上,有些时候,页面可能没这么简单。
比如说在平板设备上,用户可能左边是主页面,右边是详情页面。这种相当于啥呢?相当于羽毛球桶的羽毛球可能不是一个一个放进去的,而是有可能两个羽毛球并排着放进去。这种情况下怎么实现呢?
Nav3 提供了一个叫Stage的概念,它通过组合 NavEntry 来实现页面的显示,通过 SceneStrategy 来实现Stage的策略,这也就意味着,我们添加进页面返回栈的页面可以通过 Stage 来实现组合显示,而不是仅限于显示一个页面,比如同时显示两个或三个页面,或者根据设备尺寸来显示不同的页面。
下面文章,我们会详细介绍 Stage 和 SceneStrategy 的使用,如何通过 Stage 来实现更加灵活与复杂的多页面组合。
项目源码已上传到 Github: github.com/MengFly/com…