了解并实现基本功能

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

对导航状态进行建模

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

以红色圆圈突出显示的软件键盘操作按钮(对勾标记图标)。
图 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 并返回相应 keyNavEntry 的函数。在创建 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。此限制可确保后台或退出条目在完成退出过渡时停止执行活动工作,例如收集流或启动绑定到 RESUMEDSTARTED 状态的协程。

如何组合

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

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

综合应用

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

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

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

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

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