构建列表-详情布局

列表-详情是一种 UI 模式,由双窗格布局组成,其中一个窗格显示项目列表,另一个窗格显示从列表中选择的项目的详情。

此模式对于提供大型集合中元素的详细信息的应用特别有用,例如,电子邮件客户端可以包含电子邮件列表和每封电子邮件的详细内容。列表-详情模式也可用于不太重要的路径,例如将应用偏好设置分为类别列表,并在详情窗格中显示每个类别的偏好设置。

A detail pane shown alongside the list page.
图 1. 当屏幕尺寸足够时,详情窗格与列表窗格并排显示。
After an item is selected, the detail pane takes over the whole screen.
图 2. 当屏幕尺寸有限时,详情窗格(因为已选择某个项目)会占据整个空间。

使用 NavigableListDetailPaneScaffold 实现列表-详情模式

NavigableListDetailPaneScaffold 是一个可组合项,可简化在 Jetpack Compose 中实现列表-详情布局。它封装了 ListDetailPaneScaffold,并添加了内置导航和预测性返回动画。

列表-详情 Scaffold 最多支持三个窗格

  1. 列表窗格:显示项目集合。
  2. 详情窗格:显示所选项目的详情。
  3. 额外窗格(可选:在需要时提供额外上下文。

Scaffold 会根据窗口大小进行调整

  • 在大型窗口中,列表和详情窗格并排显示。
  • 在小型窗口中,一次只显示一个窗格,并随用户导航进行切换。

声明依赖项

NavigableListDetailPaneScaffoldMaterial 3 自适应导航库的一部分。

将以下三个相关依赖项添加到您的应用或模块的 build.gradle 文件中

Kotlin

implementation("androidx.compose.material3.adaptive:adaptive")
implementation("androidx.compose.material3.adaptive:adaptive-layout")
implementation("androidx.compose.material3.adaptive:adaptive-navigation")

Groovy

implementation 'androidx.compose.material3.adaptive:adaptive'
implementation 'androidx.compose.material3.adaptive:adaptive-layout'
implementation 'androidx.compose.material3.adaptive:adaptive-navigation'
  • adaptive: 低级构建块,例如 HingeInfoPosture
  • adaptive-layout: 自适应布局,例如 ListDetailPaneScaffoldSupportingPaneScaffold
  • adaptive-navigation: 用于在窗格内和窗格之间导航的可组合项,以及默认支持导航的自适应布局,例如 NavigableListDetailPaneScaffoldNavigableSupportingPaneScaffold

确保您的项目包含 compose-material3-adaptive 1.1.0-beta1 或更高版本

选择启用预测性返回手势

要在 Android 15 或更低版本中启用预测性返回动画,您必须选择启用对预测性返回手势的支持。要选择启用,请将 android:enableOnBackInvokedCallback="true" 添加到您的 AndroidManifest.xml 文件中的 <application> 标签或单独的 <activity> 标签中。如需了解更多信息,请参阅选择启用预测性返回手势

一旦您的应用目标平台为 Android 16 (API 级别 36) 或更高版本,预测性返回手势将默认启用。

基本用法

按如下方式实现 NavigableListDetailPaneScaffold

  1. 使用表示所选内容的类。使用 Parcelable 类来支持保存和恢复所选列表项。使用 kotlin-parcelize 插件为您生成代码。
  2. 使用 rememberListDetailPaneScaffoldNavigator 创建一个 ThreePaneScaffoldNavigator

此导航器用于在列表、详情和额外窗格之间移动。通过声明泛型类型,导航器还会跟踪 Scaffold 的状态(即正在显示哪个 MyItem)。由于此类型是 Parcelable 的,因此导航器可以保存和恢复状态,以自动处理配置更改。

  1. 将导航器传递给 NavigableListDetailPaneScaffold 可组合项。

  2. 将您的列表窗格实现提供给 NavigableListDetailPaneScaffold。使用 AnimatedPane 在导航期间应用默认窗格动画。然后使用 ThreePaneScaffoldNavigator 导航到详情窗格 ListDetailPaneScaffoldRole.Detail,并显示传递的项目。

  3. 将您的详情窗格实现包含在 NavigableListDetailPaneScaffold 中。

导航完成后,currentDestination 包含您的应用导航到的窗格,包括窗格中显示的内容。contentKey 属性与原始调用中指定的类型相同,因此您可以访问需要显示的任何数据。

  1. (可选)更改 NavigableListDetailPaneScaffold 中的 defaultBackBehavior。默认情况下,NavigableListDetailPaneScaffoldPopUntilScaffoldValueChange 用于 defaultBackBehavior

如果您的应用需要不同的返回导航模式,您可以通过指定另一个 BackNavigationBehavior 选项来覆盖此行为。

BackNavigationBehavior 选项

以下部分将以一个电子邮件应用为例,其中一个窗格中包含电子邮件列表,另一个窗格中包含详细视图。

此行为侧重于整体布局结构的变化。在多窗格设置中,更改详情窗格中的电子邮件内容不会改变底层布局结构。因此,返回按钮可能会退出应用或当前导航图,因为在当前上下文中没有要恢复的布局更改。在单窗格布局中,按下返回按钮将跳过详情视图中的内容更改并返回到列表视图,因为这表示明确的布局更改。

考虑以下示例

  • 多窗格:您正在详情窗格中查看一封电子邮件(项目 1)。点击另一封电子邮件(项目 2)会更新详情窗格,但列表和详情窗格仍可见。按下返回按钮可能会退出应用或当前的导航流。
  • 单窗格:您查看项目 1,然后是项目 2,按下返回按钮将直接返回到电子邮件列表窗格。

当您希望用户在每次返回操作时感知到明显的布局转换时,请使用此选项。

Navigation value change.
PopUntilContentChange

此行为优先考虑显示的内容。如果您查看项目 1,然后是项目 2,按下返回按钮将恢复到项目 1,无论布局如何。

考虑以下示例

  • 多窗格:您在详情窗格中查看项目 1,然后点击列表中的项目 2。详情窗格会更新。按下返回按钮将恢复详情窗格到项目 1。
  • 单窗格:发生相同的内容恢复。

当您的用户期望通过返回操作返回到之前查看的内容时,请使用此选项。

the transition between two detail panes
PopUntilCurrentDestinationChange

此行为会弹出返回堆栈,直到当前导航目标发生更改。这同样适用于单窗格和多窗格布局。

考虑以下示例

无论您处于单窗格还是多窗格布局,按下返回按钮始终会将焦点从突出显示的导航元素移动到上一个目标。在我们的电子邮件应用中,这意味着所选窗格的视觉指示将发生变化。

当保持当前导航的清晰视觉指示对于用户体验至关重要时,请使用此选项。

navigating between the detail and list panes
PopLatest

此选项仅从返回堆栈中移除最近的目标。在返回导航时,如果不想跳过中间状态,请使用此选项。

完成这些步骤后,您的代码应如下所示

val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<MyItem>()
val scope = rememberCoroutineScope()

NavigableListDetailPaneScaffold(
    navigator = scaffoldNavigator,
    listPane = {
        AnimatedPane {
            MyList(
                onItemClick = { item ->
                    // Navigate to the detail pane with the passed item
                    scope.launch {
                        scaffoldNavigator.navigateTo(
                            ListDetailPaneScaffoldRole.Detail,
                            item
                        )
                    }
                },
            )
        }
    },
    detailPane = {
        AnimatedPane {
            // Show the detail pane content if selected item is available
            scaffoldNavigator.currentDestination?.contentKey?.let {
                MyDetails(it)
            }
        }
    },
)