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

0 阅读8分钟

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

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

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

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

页面如下: 500

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

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

页面返回栈

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

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

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

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

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

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

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

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

Navigation3 中的页面

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

  1. NavKey:页面的唯一标识,不同的 Key 绑定不同页面。框架通过识别栈里的 Key,决定当前该显示哪个页面
  2. NavBackStack:页面返回栈本身,栈里装的都是 NavKey
  3. NavEntry:每个 Entry 包含一个 NavKey 和对应的 Compose 函数。框架通过 Key 找到对应的函数,渲染出页面
  4. 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:

  1. 欢迎页面:不需要参数,就是个简单的标识,用 data object 来表示就行
  2. 菜单页面:同样不需要参数,也是个标识,还是用 data object
  3. 菜品详情页面:需要根据菜品 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 函数。

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

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

// 创建导航栈,初始时只放一个 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…