了解并实现基本功能

导航描述了用户在应用中移动的方式。用户通常通过点按或点击界面元素与其互动,而应用会通过显示新内容来做出响应。如果用户想要返回上一个内容,可以使用返回手势操作或点按返回按钮。

对导航状态进行建模

对这种行为进行建模的一种便捷方式是使用内容堆栈。当用户向前导航到新内容时,新内容会被推送到堆栈顶部。当用户从该内容返回时,该内容会从堆栈中弹出,并显示之前的内容。在导航方面,此堆栈通常称为“返回堆栈”,因为它表示用户可以返回的内容。

以红色圆圈突出显示的软件键盘操作按钮(对勾标记图标)。
图 1. 显示返回堆栈如何随用户导航事件而变化的图表。

创建返回堆栈

在 Navigation 3 中,返回堆栈实际上不包含内容。相反,它包含对内容(称为“键”)的引用。键可以是任何类型,但通常是简单的可序列化数据类。使用引用而非内容具有以下优势:

  • 只需将键推送到返回堆栈,即可轻松进行导航。
  • 只要键可序列化,就可以将返回栈保存到持久性存储空间,从而使其在配置更改和进程终止后继续存在。这一点很重要,因为用户希望能够离开您的应用,稍后再返回该应用,并从上次离开时显示的同一内容处继续操作。如需了解详情,请参阅保存返回堆栈。

Navigation 3 API 中的一个关键概念是,您拥有返回堆栈。该库:

  • 预期您的返回堆栈将是受快照状态支持的 List<T>,其中 T 是返回堆栈 keys 的类型。您可以使用 Any,也可以提供自己的更强类型的键。当您看到“推送”或“弹出”这两个术语时,其底层实现是从列表末尾添加或移除项。
  • 观察您的返回堆栈,并使用 NavDisplay 在界面中反映其状态。

以下示例展示了如何创建键和返回堆栈,以及如何响应用户导航事件来修改返回堆栈:

// Define keys that will identify content
data object ProductList
data class ProductDetail(val id: String)

@Composable
fun MyApp() {

    // Create a back stack, specifying the key the app should start with
    val backStack = remember { mutableStateListOf<Any>(ProductList) }

    // Supply your back stack to a NavDisplay so it can reflect changes in the UI
    // ...more on this below...

    // Push a key onto the back stack (navigate forward), the navigation library will reflect the change in state
    backStack.add(ProductDetail(id = "ABC"))

    // Pop a key off the back stack (navigate back), the navigation library will reflect the change in state
    backStack.removeLastOrNull()
}

解析内容密钥

在 Navigation 3 中,内容使用 NavEntry 进行建模,这是一个包含可组合函数的类。它表示一个目的地,即用户可以向前和向后导航到的单个内容片段。

NavEntry 还可以包含元数据(有关内容的信息)。容器对象(例如 NavDisplay)可以读取此元数据,以帮助它们决定如何显示 NavEntry 的内容。例如,元数据可用于替换特定 NavEntry 的默认动画。NavEntry metadata 是一个从 String 键到 Any 值的映射,可提供多用途的数据存储。

如需将 key 转换为 NavEntry,请创建 Entry Provider。这是一个接受 key 并返回相应 key 的 NavEntry 的函数。在创建 NavDisplay 时,它通常定义为 lambda 参数。

您可以通过以下两种方式创建 Entry Provider:直接创建 lambda 函数,或使用 entryProvider DSL。

直接创建入口提供程序函数

您通常使用 when 语句创建一个 Entry Provider 函数,并为每个键创建一个分支。

entryProvider = { key ->
    when (key) {
        is ProductList -> NavEntry(key) { Text("Product List") }
        is ProductDetail -> NavEntry(
            key,
            metadata = mapOf("extraDataKey" to "extraDataValue")
        ) { Text("Product ${key.id} ") }

        else -> {
            NavEntry(Unit) { Text(text = "Invalid Key: $it") }
        }
    }
}

使用 entryProvider DSL

entryProvider DSL 可避免需要针对每种键类型进行测试,并为每种键类型构建 NavEntry,从而简化您的 lambda 函数。为此,请使用 entryProvider 构建器函数。如果找不到键,它还会包含默认后备行为(抛出错误)。

entryProvider = entryProvider {
    entry<ProductList> { Text("Product List") }
    entry<ProductDetail>(
        metadata = mapOf("extraDataKey" to "extraDataValue")
    ) { key -> Text("Product ${key.id} ") }
}

请注意以下代码段中的内容:

  • entry 用于定义具有给定类型和可组合内容的 NavEntry
  • entry 接受 metadata 参数来设置 NavEntry.metadata

显示返回堆栈

返回堆栈表示应用的导航状态。每当返回堆栈发生变化时,应用界面都应反映新的返回堆栈状态。在 Navigation 3 中,NavDisplay 会观察您的返回堆栈并相应地更新其界面。使用以下参数构建它:

  • 您的返回堆栈 - 这应为 SnapshotStateList<T> 类型,其中 T 是返回堆栈键的类型。它是可观测的 List,因此当它发生变化时,会触发 NavDisplay 的重组。
  • 用于将返回堆栈中的键转换为 NavEntry 对象的 entryProvider。
  • (可选)为 onBack 参数提供 lambda。当用户触发返回事件时,系统会调用此方法。

以下示例展示了如何创建 NavDisplay。

data object Home
data class Product(val id: String)

@Composable
fun NavExample() {

    val backStack = remember { mutableStateListOf<Any>(Home) }

    NavDisplay(
        backStack = backStack,
        onBack = { backStack.removeLastOrNull() },
        entryProvider = { key ->
            when (key) {
                is Home -> NavEntry(key) {
                    ContentGreen("Welcome to Nav3") {
                        Button(onClick = {
                            backStack.add(Product("123"))
                        }) {
                            Text("Click to navigate")
                        }
                    }
                }

                is Product -> NavEntry(key) {
                    ContentBlue("Product ${key.id} ")
                }

                else -> NavEntry(Unit) { Text("Unknown route") }
            }
        }
    )
}

默认情况下,NavDisplay 会在单窗格布局中显示返回堆栈中最顶层的 NavEntry。以下录制内容展示了此应用的运行情况:

具有两个目的地的 `NavDisplay` 默认行为。
图 2. NavDisplay 具有两个目的地的默认行为。

目标生命周期

NavDisplay 使用自定义 LifecycleOwner,根据场景级限制条件和入口级限制条件来限制 NavEntry 的生命周期状态。

如需详细了解 Compose 中的生命周期,请参阅 Jetpack Compose 中的生命周期。

场景级生命周期限制

NavDisplay 管理活跃 Scene 的生命周期。场景级上限的确定方式如下:

对于非叠加场景:

  • RESUMED:仅当场景转场已完成且没有活跃的叠加场景显示在其顶部时才允许。
  • STARTED:在场景过渡期间(例如向前或向后导航时,或者被叠加层覆盖时)上限为 STARTED。

对于叠加场景(例如对话框或底部动作条):

  • RESUMED:仅允许用于最顶层、当前活跃的叠加层场景。
  • STARTED:对于被较新叠加层覆盖的任何底层叠加层场景,上限为 STARTED。

入门级生命周期状态

该库会根据每个 NavEntry 在返回堆栈中的存在情况来管理其最大生命周期状态:

  • RESUMED:如果条目存在于当前返回堆栈中,则允许其生命周期达到 RESUMED(受场景级上限限制)。
  • CREATED:如果条目不再位于返回堆栈中(例如,当条目被弹出但仍在屏幕上渲染并以动画形式退出时),库会严格将条目的生命周期限制为 CREATED。此限制可确保后台或退出条目在完成退出过渡时停止执行活动工作,例如收集流或启动绑定到 RESUMED 或 STARTED 状态的协程。

如何组合

例如,NavEntry 的最终生命周期状态按如下方式解析:

场景 场景级上限 入门级上限 有效上限
活跃条目,已结算的界面(无过渡效果或叠加层) RESUMED RESUMED RESUMED
过渡期间(导航到或从)的有效条目 STARTED RESUMED STARTED
有效条目,被叠加层遮盖(例如,对话框处于打开状态) STARTED RESUMED STARTED
弹出条目,动画效果为淡出 STARTED 或 RESUMED CREATED CREATED

综合应用

下图显示了数据如何在 Navigation 3 中的各个对象之间流动:

直观呈现了 Navigation 3 中各个对象之间的数据流。
图 3. 图表显示了数据如何在 Navigation 3 中的各种对象之间流动。
  1. 导航事件会触发更改。密钥会根据用户互动添加到返回堆栈或从返回堆栈中移除。

  2. 返回堆栈状态发生变化时,触发内容检索。NavDisplay(用于呈现返回堆栈的可组合项)会观察返回堆栈。在默认配置中,它会在单窗格布局中显示最顶层的返回堆栈条目。当返回栈顶部的键发生变化时,NavDisplay 会使用此键从条目提供程序请求相应的内容。

  3. entry 提供方提供内容。条目提供程序是将键解析为 NavEntry 的函数。在从 NavDisplay 收到密钥后,条目提供方会提供关联的 NavEntry,其中包含密钥和内容。

  4. 内容显示。NavDisplay 接收 NavEntry 并显示内容。