Compose Navigation 3 深度解析(二):基础用法

在上篇文章 Compose-Navigation3-为什么会有Navigation3 中,咱们介绍了为什么会有 Navigation3 这么一个导航框架,简单来说就是在现代数据驱动页面的声明式 UI 框架的趋势下,单页面应用(SPA)已经渐渐成为一种主流,Navigation3 就是为了适应 SPA 而生的。

再次强调 Navigation3 的作用: 管理场景跳转以及组合显示页面为场景。

这篇文章以具体的案例讲一讲 Navigation3 的基础用法,基础用法重点在页面 (场景)跳转,我们以页面跳转为切入点。写一个简单的 Demo,功能如下:

  1. 包含三个页面:欢迎页面、菜单页面和菜品详情页面。
  2. 用户进入 APP,进入欢迎页面,2 秒后自动进入菜单页面
  3. 用户点击菜品,进入菜品详情页面。
  4. 滑动返回或点击返回按钮返回主菜单页面。

页面如下:

通过这么一个简单的 Demo,咱们来盘一盘 Navigation3 的基础用法:

  1. 如何组合多个页面
  2. 页面间如何传递参数
  3. 如何通过控制页面返回栈控制页面跳转

页面返回栈

Navigation3 使用页面返回栈来组织页面显示,要想理解 Navigation3,首先得搞懂页面返回栈是个啥。得从栈这个数据结构说起:

栈这种数据结构,先入后出,或者说后入先出。啥意思呢?一个栈可以理解为羽毛球筒,最先放进羽毛球桶的羽毛球,最后才能拿出来,最后放进羽毛球桶的羽毛球最先拿出来。这是不是就好理解了呢?

那么为什么要使用栈这种数据结构呢?

我们先来看看一个页面导航要考虑那些东西:

  1. 一个页面导航要决定哪个页面显示在最上层。
  2. 一个页面导航要记录页面跳转历史,就好像是浏览器的浏览历史一样,点击返回按钮的时候,能够返回上一个页面。

使用栈这种数据结构,可以很容易实现上面的逻辑

  1. 栈顶的内容(羽毛球桶最上面的羽毛球)就是当前可以看到的页面(下面的被遮住了肯定看不到)
  2. 退出当前页面的时候,相当于把羽毛球桶最上面的羽毛球拿了出来,就能看到被压在下面的羽毛球了,被压在下面的羽毛球就是页面被加到里面的顺序,也就能保证记录页面的跳转历史了。

这也就是页面返回栈的原理了,不单单是 Navigation3 中使用了这种数据结构,最开始的 Activity 栈也是这种模式,包括浏览器的返回功能底层使用的也是栈数据结构。

Navigation3 同样用栈来组织页面,但具体管理方式有自己的一套玩法。咱们先把几个核心概念整明白:

  1. NavKey:页面的唯一标识,不同的 Key 绑定不同页面。框架通过识别栈里的 Key,决定当前该显示哪个页面
  2. NavBackStack:页面返回栈本身,栈里装的都是 NavKey
  3. NavEntry:每个 Entry 包含一个 NavKey 和对应的 Compose 函数。框架通过 Key 找到对应的函数,渲染出页面
  4. NavDisplay:Navigation3 的核心组件,导航的所有定义都在这儿完成

添加依赖

libs.versions.toml

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

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)
}

Navigation3 和 Navigation2 的一个重要区别在于 ------ Navigation3 不通过 NavController 来间接管理导航状态,而是直接将页面返回栈的控制权交给开发者,让开发者可以更灵活地操作栈。简单来说:

  • 往栈里加个 NavKey,就相当于打开一个新页面
  • 从栈里移除个 Key,就相当于关闭当前页面

但是这个栈还不是简单地栈,它允许你像一个列表一样进行操作,所以给了我们很大的灵活度。

创建导航很简单,首先创建一个页面返回栈,再使用 NavDisplay 组件来配置导航行为。

kotlin 复制代码
// 创建一个页面返回栈
val navBackStack = rememberNavBackStack()

NavDisplay(
    // 将页面返回栈传递给 NavDisplay 组件
    backStack = navBackStack,
    // 我们希望监听返回事件,使用 onBack 来监听
    // 其实默认的逻辑和下面的逻辑一致,这里写出来只是为了演示
    onBack = {
        // 在返回时,移除栈顶的页面,也就是显示上一个页面
        navBackStack.removeLastOrNull()
    },
)

现在咱们已经创建了 NavDisplay 组件,但它还只是个空壳子,因为还没往里面加页面。

前面说过,页面由 NavEntry 表示,每个 Entry 都包含一个 NavKey 和对应的 Compose 函数。框架就靠识别 NavKey 来决定当前该显示哪个页面。

咱们的 Demo 需要三个页面,所以得创建三个对应的 NavKey:

  1. 欢迎页面 :不需要参数,就是个简单的标识,用 data object 来表示就行
  2. 菜单页面 :同样不需要参数,也是个标识,还是用 data object
  3. 菜品详情页面 :需要根据菜品 ID 来显示不同内容,所以得用 data class 来承载这个参数
kotlin 复制代码
/**
 * 欢迎页面Key
 */
@Serializable
data object Welcome : NavKey

/**
 * 主页Key
 */
@Serializable
data object Home : NavKey

/**
 * 菜品详情
 */
@Serializable
data class Detail(val id: Int) : NavKey

对此我们需要创建三个页面组件,分别对应欢迎页面、菜单页面和菜品详情页面。页面内容非重点,就不再详细描述了,只简单描述页面参数和功能,具体页面源码可以在 ComposeSample 这里找到。

kotlin 复制代码
/**  
 * 欢迎页面  
 *  
 * 该页面启动后,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 函数。

同时,咱们还得把页面跳转的逻辑理清楚:

  1. 初始状态 :应用启动时,应该显示欢迎页面。所以导航栈初始化时,里面只放一个 Welcome Key
  2. 欢迎页跳转 :欢迎页显示2秒后,自动跳转到菜单页。这时候需要把 Home Key 加到导航栈里
  3. 清理欢迎页 :跳转后,得把 Welcome Key 从栈里移除, 不然用户在主页点返回,又会回到欢迎页,这体验就太拉胯了
  4. 点击菜品 :用户在菜单页点击某个菜品,就把对应的 Detail Key 加到栈里,显示详情页
  5. 返回操作 :用户在详情页滑动返回,就把 Detail Key 从栈里移除,回到菜单页
kotlin 复制代码
// 创建导航栈,初始时只放一个 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...

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui