构建具有动态导航的自适应应用

1. 简介

在 Android 平台上开发应用程序的一大优势是能够接触到各种形态因素的用户,例如可穿戴设备、折叠屏手机、平板电脑、桌面电脑,甚至电视。当用户使用应用程序时,他们可能希望在大屏幕设备上使用同一应用程序以利用更大的屏幕空间。越来越多的 Android 用户在各种屏幕尺寸的多个设备上使用他们的应用程序,并期望在所有设备上都能获得高质量的用户体验。

到目前为止,您已经学习了如何主要为移动设备创建应用程序。在本 Codelab 中,您将学习如何转换您的应用程序以使其适应其他屏幕尺寸。您将使用自适应导航布局模式,这些模式对于移动设备和大屏幕设备(如折叠屏手机、平板电脑和桌面电脑)来说都美观且易用。

先决条件

  • 熟悉 Kotlin 编程,包括类、函数和条件语句
  • 熟悉使用 ViewModel
  • 熟悉创建 Composable 函数
  • 有使用 Jetpack Compose 构建布局的经验
  • 有在设备或模拟器上运行应用程序的经验

您将学到什么

  • 如何为简单的应用程序在屏幕之间创建导航,而无需使用 Navigation Graph
  • 如何使用 Jetpack Compose 创建自适应导航布局
  • 如何创建自定义后退处理程序

您将构建什么

  • 您将在现有的 Reply 应用程序中实现动态导航,以使其布局适应所有屏幕尺寸

最终产品将如下面的图片所示

56cfa13ef31d0b59.png

​​

您需要什么

  • 一台具有互联网访问权限的电脑、一个网络浏览器和 Android Studio
  • 访问 GitHub

2. 应用概述

Reply 应用简介

Reply 应用是一个多屏幕应用,类似于 电子邮件客户端

a1af0f9193718abf.png

它包含 4 个不同的类别,通过不同的选项卡显示,分别是:收件箱、已发送、草稿和垃圾邮件。

下载初始代码

在 Android Studio 中,打开 basic-android-kotlin-compose-training-reply-app 文件夹。

3. 初始代码演练

Reply 应用中的重要目录

The Reply App file directory displays two sub-directories that are expanded:

Reply 应用项目的數據和 UI 层被分隔到不同的目录中。 ReplyViewModelReplyUiState 和其他可组合项位于 ui 目录中。定义数据层和数据提供程序类的 dataenum 类位于 data 目录中。

Reply 应用中的数据初始化

Reply 应用通过 ReplyViewModel 中的 initializeUIState() 方法初始化数据,该方法在 init 函数中执行。

ReplyViewModel.kt

...
    init {
        initializeUIState()
    }
 

    private fun initializeUIState() {
        var mailboxes: Map<MailboxType, List<Email>> =
            LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
        _uiState.value = ReplyUiState(
            mailboxes = mailboxes,
            currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
                ?: LocalEmailsDataProvider.defaultEmail
        )
    }
...

屏幕级可组合项

与其他应用程序一样,Reply 应用使用 ReplyApp 可组合项作为主可组合项,其中声明了 viewModeluiState。各种 viewModel() 函数也作为 lambda 参数传递给 ReplyHomeScreen 可组合项。

ReplyApp.kt

...
@Composable
fun ReplyApp(modifier: Modifier = Modifier) {
    val viewModel: ReplyViewModel = viewModel()
    val replyUiState = viewModel.uiState.collectAsState().value

    ReplyHomeScreen(
        replyUiState = replyUiState,
        onTabPressed = { mailboxType: MailboxType ->
            viewModel.updateCurrentMailbox(mailboxType = mailboxType)
            viewModel.resetHomeScreenStates()
        },
        onEmailCardPressed = { email: Email ->
            viewModel.updateDetailsScreenStates(
                email = email
            )
        },
        onDetailScreenBackPressed = {
            viewModel.resetHomeScreenStates()
        },
        modifier = modifier
    )
}

其他可组合项

  • ReplyHomeScreen.kt:包含主屏幕的屏幕可组合项,包括导航元素。
  • ReplyHomeContent.kt:包含定义主屏幕更详细的可组合项的可组合项。
  • ReplyDetailsScreen.kt:包含详细信息屏幕的屏幕可组合项和较小的可组合项。

请随时详细浏览每个文件,以在继续 Codelab 的下一部分之前更好地了解可组合项。

4. 在没有导航图的情况下更改屏幕

在之前的路径中,您学习了如何使用 NavHostController 类在一个屏幕之间导航到另一个屏幕。使用 Compose,您还可以通过利用运行时可变状态,使用简单的条件语句更改屏幕。这在像 Reply 应用这样的小型应用程序中特别有用,在这些应用程序中,您只需要在两个屏幕之间切换。

使用状态更改更改屏幕

在 Compose 中,当状态发生更改时,屏幕会重新组合。您可以使用简单的条件语句来响应状态更改,从而更改屏幕。

您将使用条件语句在用户位于主屏幕时显示主屏幕的内容,在用户不位于主屏幕时显示详细信息屏幕。

通过完成以下步骤修改 Reply 应用以允许在状态更改时更改屏幕

  1. 在 Android Studio 中打开初始代码。
  2. ReplyHomeScreen.kt 中的 ReplyHomeScreen 可组合项中,使用 if 语句将 ReplyAppContent 可组合项包装起来,以用于 replyUiState 对象的 isShowingHomepage 属性为 true 的情况。

ReplyHomeScreen.kt

@Composable
fun ReplyHomeScreen(
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit,
    onEmailCardPressed: (Int) -> Unit,
    onDetailScreenBackPressed: () -> Unit,
    modifier: Modifier = Modifier
) {

...
    if (replyUiState.isShowingHomepage) {
        ReplyAppContent(
            replyUiState = replyUiState,
            onTabPressed = onTabPressed,
            onEmailCardPressed = onEmailCardPressed,
            navigationItemContentList = navigationItemContentList,
            modifier = modifier

        )
    }
}

您现在必须考虑用户不在主屏幕时的情况,方法是显示详细信息屏幕。

  1. 添加一个 else 分支,并在其主体中使用 ReplyDetailsScreen 可组合项。将 replyUIStateonDetailScreenBackPressedmodifier 作为参数添加到 ReplyDetailsScreen 可组合项中。

ReplyHomeScreen.kt

@Composable
fun ReplyHomeScreen(
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit,
    onEmailCardPressed: (Int) -> Unit,
    onDetailScreenBackPressed: () -> Unit,
    modifier: Modifier = Modifier
) {

...

    if (replyUiState.isShowingHomepage) {
        ReplyAppContent(
            replyUiState = replyUiState,
            onTabPressed = onTabPressed,
            onEmailCardPressed = onEmailCardPressed,
            navigationItemContentList = navigationItemContentList,
            modifier = modifier

        )
    } else {
        ReplyDetailsScreen(
            replyUiState = replyUiState,
            onBackPressed = onDetailScreenBackPressed,
            modifier = modifier
        )
    }
}

replyUiState 对象是一个状态对象。因此,当 replyUiState 对象的 isShowingHomepage 属性发生变化时,ReplyHomeScreen 可组合项会重新组合,并且 if/else 语句在运行时重新评估。这种方法支持在不同屏幕之间导航,而无需使用 NavHostController 类。

8443a3ef1a239f6e.gif

创建自定义后退处理程序

使用 NavHost 可组合项在屏幕之间切换的一个优点是,先前屏幕的方向保存在后退栈中。这些保存的屏幕允许系统后退按钮在调用时轻松导航到上一个屏幕。由于 Reply 应用不使用 NavHost,因此您必须手动添加代码来处理后退按钮。您将在下一步中执行此操作。

完成以下步骤,在 Reply 应用中创建自定义后退处理程序

  1. ReplyDetailsScreen 可组合项的第一行,添加一个 BackHandler 可组合项。
  2. BackHandler 可组合项的主体中调用 onBackPressed() 函数。

ReplyDetailsScreen.kt

...
import androidx.activity.compose.BackHandler
...
@Composable
fun ReplyDetailsScreen(
    replyUiState: ReplyUiState,
    onBackPressed: () -> Unit,
    modifier: Modifier = Modifier
) {
    BackHandler {
        onBackPressed()
    }
... 

5. 在大屏幕设备上运行应用

使用可调整大小的模拟器检查您的应用

为了创建可用的应用,开发人员需要了解用户在各种形态因素下的体验。因此,您必须从开发过程的开始就测试各种形态因素下的应用。

您可以使用许多不同屏幕尺寸的模拟器来实现此目标。但是,这样做可能很麻烦,尤其是在您同时为多个屏幕尺寸构建时。您可能还需要测试正在运行的应用程序如何响应屏幕尺寸更改,例如方向更改、桌面上的窗口尺寸更改以及折叠屏手机上的折叠状态更改。

Android Studio 通过引入 **可调整大小的模拟器** 来帮助您测试这些场景。

完成以下步骤以设置可调整大小的模拟器

  1. 在 Android Studio 中,选择 **工具** > **设备管理器**。

The Tools menu displays a list of options. Device Manager , which appears halfway down the list, is selected.

  1. 在 **设备管理器** 中,单击 **+** 图标以创建虚拟设备。

The device manager toolbar displays two menu options including create virtual device.

  1. 选择 **手机** 类别和 **可调整大小(实验性)** 设备。
  2. 单击 **下一步**。

The Device Manager window displays a prompt to choose a device definition. A list of options displays with a search field above it. The category

  1. 选择 **API 级别 34** 或更高版本。
  2. 单击 **下一步**。

The Virtual Device Configuration window displays a prompt to select a system image. 34 API level is selected.

  1. 命名您的新 Android 虚拟设备。
  2. 单击 **完成**。

The Virtual Configration screen in Android Virtural Device (AVD) displays. The configuration screen includes a text field to enter the AVD name. Below the name field are a list of device options, including the device definition (Resizable Experimental), the system image (Tiramisu), and the orientation, with Portrait orientation selected by default. Buttons reading

在大屏幕模拟器上运行应用

现在您已经设置了可调整大小的模拟器,让我们看看应用程序在大屏幕上的显示效果。

  1. 在可调整大小的模拟器上运行应用程序。
  2. 为显示模式选择 **平板电脑**。

bfacf9c20a30b06b.png

  1. 检查平板电脑模式下的横向模式下的应用程序。

bb0fa5e954f6ca4b.png

请注意,平板电脑屏幕显示是水平拉长的。虽然这种方向在功能上可以工作,但它可能不是大屏幕空间的最佳利用方式。让我们在下一步中解决这个问题。

为大屏幕设计

当您在平板电脑上查看此应用程序时,您首先想到的可能是它设计糟糕且不吸引人。您是对的:此布局**并非**为大屏幕使用而设计。

在为大屏幕(如平板电脑和折叠屏手机)设计时,您必须考虑用户工效学以及用户手指与屏幕的距离。对于移动设备,用户的手指可以轻松触及大部分屏幕;交互式元素(如按钮和导航元素)的位置并不那么关键。但是,对于大屏幕,将关键的交互式元素放在屏幕中间可能会使它们难以触及。

如您在 Reply 应用中看到的,为大屏幕设计不仅仅是拉伸或放大 UI 元素以适应屏幕。它是一个机会,可以使用增加的屏幕空间为您的用户创造不同的体验。例如,您可以在同一屏幕上添加另一个布局,以避免需要导航到另一个屏幕或使多任务处理成为可能。

f50e77a4ffd923a.png

这种设计可以提高用户的工作效率并促进更大的参与度。但在部署此设计之前,您必须首先学习如何为不同的屏幕尺寸创建不同的布局。

6. 使您的布局适应不同的屏幕尺寸

什么是断点?

您可能想知道如何为同一个应用程序显示不同的布局。简而言之,就是根据不同的状态使用条件语句,就像您在本 Codelab 的开头所做的那样。

要创建自适应应用程序,您需要布局根据屏幕尺寸更改。布局更改的测量点称为断点。Material Design 创建了一个 有见地的断点范围,涵盖大多数 Android 屏幕。

A table shows the breakpoint range (in dp) for different device types and setups. 0 to 599 dp is for handsets in portait mode, phones in landscape, compact window size, 4 columns, and 8 minimum margins. 600 to 839 dp is for foldable small tablets in portait or landscape modes, medium window size class, 12 columns, and 12 minimum margins. 840 dp or greater is for a large tablet in portrait or landscape modes, the expanded window size class, 12 columns, and 32 minimum margins. Table notes state that margins and gutters are flexible and don't need to be equal in size and that phones in landscape are considered an exception to still fit within the 0 to 599 dp breakpoint range.

此断点范围表显示,例如,如果您的应用程序当前正在屏幕尺寸小于 600 dp 的设备上运行,则应显示移动布局。

使用窗口大小类

为 Compose 引入的 WindowSizeClass API 使 Material Design 断点的实现更加简单。

窗口大小类为宽度和高度引入了三种尺寸类别:紧凑、中等和扩展。

The diagram represents the width-based window size classes. The diagram represents the height-based window size classes.

完成以下步骤,在 Reply 应用中实现 WindowSizeClass API

  1. material3-window-size-class 依赖项添加到模块 build.gradle.kts 文件中。

build.gradle.kts

...
dependencies {
...
    implementation("androidx.compose.material3:material3-window-size-class")
...
  1. 添加依赖项后,单击 **立即同步** 以同步 Gradle。

b4c912a45fa8b7f4.png

在更新了build.gradle.kts文件后,您现在可以创建一个变量,用于在任何给定时间存储应用窗口的大小。

  1. MainActivity.kt 文件中的 onCreate() 函数中,将 calculateWindowSizeClass() 方法(传入参数 this 上下文)赋值给名为 windowSize 的变量。
  2. 导入相应的 calculateWindowSizeClass 包。

MainActivity.kt

...
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass

...

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
        ReplyTheme {
            val layoutDirection = LocalLayoutDirection.current
            Surface (
               // ...
            ) {
                val windowSize = calculateWindowSizeClass(this)
                ReplyApp()
...  
  1. 注意 calculateWindowSizeClass 语法的红色下划线,它显示了红色灯泡。点击 windowSize 变量左侧的红色灯泡,然后选择**在“onCreate”上选择“ExperimentalMaterial3WindowSizeClassApi”**,以在 onCreate() 方法顶部创建一个注解。

f8029f61dfad0306.png

您可以在 MainActivity.kt 中使用 WindowWidthSizeClass 变量来确定在各种可组合项中显示哪个布局。让我们准备 ReplyApp 可组合项以接收此值。

  1. ReplyApp.kt 文件中,修改 ReplyApp 可组合项以接受 WindowWidthSizeClass 作为参数,并导入相应的包。

ReplyApp.kt

...
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
...

@Composable
fun ReplyApp(
    windowSize: WindowWidthSizeClass,
    modifier: Modifier = Modifier
) {
...  
  1. MainActivity.kt 文件的 onCreate() 方法中,将 windowSize 变量传递给 ReplyApp 组件。

MainActivity.kt

...
        setContent {
            ReplyTheme {
                Surface {
                    val windowSize = calculateWindowSizeClass(this)
                    ReplyApp(
                        windowSize = windowSize.widthSizeClass
                    )
...  

您还需要更新应用的预览,以便包含 windowSize 参数。

  1. WindowWidthSizeClass.Compact 作为 windowSize 参数传递给预览组件的 ReplyApp 可组合项,并导入相应的包。

MainActivity.kt

...
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
...

@Preview(showBackground = true)
@Composable
fun ReplyAppCompactPreview() {
    ReplyTheme {
        Surface {
            ReplyApp(
                windowSize = WindowWidthSizeClass.Compact,
            )
        }
    }
}
  1. 要根据屏幕大小更改应用布局,请根据 WindowWidthSizeClass 值在 ReplyApp 可组合项中添加一个 when 语句。

ReplyApp.kt

...

@Composable
fun ReplyApp(
    windowSize: WindowWidthSizeClass,
    modifier: Modifier = Modifier
) {
    val viewModel: ReplyViewModel = viewModel()
    val replyUiState = viewModel.uiState.collectAsState().value
    
    when (windowSize) {
        WindowWidthSizeClass.Compact -> {
        }
        WindowWidthSizeClass.Medium -> {
        }
        WindowWidthSizeClass.Expanded -> {
        }
        else -> {
        }
    }
...  

至此,您已建立了一个基础,可以使用 WindowSizeClass 值来更改应用中的布局。下一步是确定您希望应用在不同屏幕尺寸上如何显示。

7. 实现自适应导航布局

实现自适应 UI 导航

当前,底部导航 用于所有屏幕尺寸。

f39984211e4dd665.png

如前所述,此导航元素并不理想,因为用户可能会发现难以在较大的屏幕上触达这些基本导航元素。幸运的是,响应式 UI 的导航 中为各种窗口大小类推荐了不同的导航元素模式。对于回复应用,您可以实现以下元素

A table lists the window size classes and the few items that display. Compact width displays a bottom navigation bar. Medium width displays a navigation rail. Expanded width displays a persistent navigation drawer with a leading edge.

导航栏Material Design 提供的另一个导航组件,它允许从应用侧面访问主要目的地的紧凑导航选项。

1c73d20ace67811c.png

类似地,持久/永久导航抽屉Material Design 创建,作为另一个选项,为较大的屏幕提供符合人体工程学的访问方式。

6795fb31e6d4a564.png

实现导航抽屉

要为扩展屏幕创建导航抽屉,您可以使用 navigationType 参数。请完成以下步骤以执行此操作

  1. 为了表示不同类型的导航元素,请在新的包 utils 中创建一个新文件 WindowStateUtils.kt,该包位于 ui 目录下。
  2. 添加一个 Enum 类来表示不同类型的导航元素。

WindowStateUtils.kt

package com.example.reply.ui.utils

enum class ReplyNavigationType {
    BOTTOM_NAVIGATION, NAVIGATION_RAIL, PERMANENT_NAVIGATION_DRAWER
}
 

要成功实现导航抽屉,您需要根据应用的窗口大小确定导航类型。

  1. ReplyApp 可组合项中,创建一个 navigationType 变量,并根据 when 语句中的屏幕大小为其分配相应的 ReplyNavigationType 值。

ReplyApp.kt

...
import com.example.reply.ui.utils.ReplyNavigationType
...
    val navigationType: ReplyNavigationType
    when (windowSize) {
        WindowWidthSizeClass.Compact -> {
            navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
        }
        WindowWidthSizeClass.Medium -> {
            navigationType = ReplyNavigationType.NAVIGATION_RAIL
        }
        WindowWidthSizeClass.Expanded -> {
            navigationType = ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        }
        else -> {
            navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
        }
    }
...
 

您可以在 ReplyHomeScreen 可组合项中使用 navigationType 值。您可以通过将其设为可组合项的参数来为此做好准备。

  1. ReplyHomeScreen 可组合项中,添加 navigationType 作为参数。

ReplyHomeScreen.kt

...
@Composable
fun ReplyHomeScreen(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit,
    onEmailCardPressed: (Email) -> Unit,
    onDetailScreenBackPressed: () -> Unit,
    modifier: Modifier = Modifier
) 

...
 
  1. navigationType 传递到 ReplyHomeScreen 可组合项。

ReplyApp.kt

...
    ReplyHomeScreen(
        navigationType = navigationType,
        replyUiState = replyUiState,
        onTabPressed = { mailboxType: MailboxType ->
            viewModel.updateCurrentMailbox(mailboxType = mailboxType)
            viewModel.resetHomeScreenStates()
        },
        onEmailCardPressed = { email: Email ->
            viewModel.updateDetailsScreenStates(
                email = email
            )
        },
        onDetailScreenBackPressed = {
            viewModel.resetHomeScreenStates()
        },
        modifier = modifier
    )
...
 

接下来,您可以创建一个分支,以便在用户在扩展屏幕上打开应用并显示主屏幕时,使用导航抽屉显示应用内容。

  1. ReplyHomeScreen 可组合项主体中,为 navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER && replyUiState.isShowingHomepage 条件添加一个 if 语句。

ReplyHomeScreen.kt

import androidx.compose.material3.PermanentNavigationDrawer
...
@Composable
fun ReplyHomeScreen(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: (MailboxType) -> Unit,
    onEmailCardPressed: (Email) -> Unit,
    onDetailScreenBackPressed: () -> Unit,
    modifier: Modifier = Modifier
) {
...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        && replyUiState.isShowingHomepage
    ) {
    }

    if (replyUiState.isShowingHomepage) {
        ReplyAppContent(
            replyUiState = replyUiState,
...
  1. 要创建永久抽屉,请在 if 语句的主体中创建 PermanentNavigationDrawer 可组合项,并将 NavigationDrawerContent 可组合项作为 drawerContent 参数的输入。
  2. ReplyAppContent 可组合项作为 PermanentNavigationDrawer 的最终 lambda 参数。

ReplyHomeScreen.kt

...
    if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        && replyUiState.isShowingHomepage
    ) {
        PermanentNavigationDrawer(
            drawerContent = {
                PermanentDrawerSheet(Modifier.width(dimensionResource(R.dimen.drawer_width))) {
                    NavigationDrawerContent(
                        selectedDestination = replyUiState.currentMailbox,
                        onTabPressed = onTabPressed,
                        navigationItemContentList = navigationItemContentList,
                        modifier = Modifier
                            .wrapContentWidth()
                            .fillMaxHeight()
                            .background(MaterialTheme.colorScheme.inverseOnSurface)
                            .padding(dimensionResource(R.dimen.drawer_padding_content))
                    )
                }
            }
        ) {
            ReplyAppContent(
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        }
    }

...
  1. 添加一个 else 分支,该分支使用以前的可组合项主体来维护非扩展屏幕的先前分支。

ReplyHomeScreen.kt

...
if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
        && replyUiState.isShowingHomepage
) {
        PermanentNavigationDrawer(
            drawerContent = {
                PermanentDrawerSheet(Modifier.width(dimensionResource(R.dimen.drawer_width))) {
                    NavigationDrawerContent(
                        selectedDestination = replyUiState.currentMailbox,
                        onTabPressed = onTabPressed,
                        navigationItemContentList = navigationItemContentList,
                        modifier = Modifier
                            .wrapContentWidth()
                            .fillMaxHeight()
                            .background(MaterialTheme.colorScheme.inverseOnSurface)
                            .padding(dimensionResource(R.dimen.drawer_padding_content))
                    )
                }
            }
        ) {
            ReplyAppContent(
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        }
    } else {
        if (replyUiState.isShowingHomepage) {
            ReplyAppContent(
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        } else {
            ReplyDetailsScreen(
                replyUiState = replyUiState,
                onBackPressed = onDetailScreenBackPressed,
                modifier = modifier
            )
        }
    }
}
...
  1. 在**平板电脑**模式下运行应用。您应该会看到以下屏幕

2dbbc2f88d08f6a.png

实现导航栏

与导航抽屉实现类似,您需要使用 navigationType 参数在导航元素之间切换。

首先,让我们为中等屏幕添加一个导航栏。

  1. 首先准备 ReplyAppContent 可组合项,方法是添加 navigationType 作为参数。

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit),
    onEmailCardPressed: (Email) -> Unit,
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {       
... 
  1. navigationType 值传递到两个 ReplyAppContent 可组合项。

ReplyHomeScreen.kt

...
            ReplyAppContent(
                navigationType = navigationType,
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
        }
    } else {
        if (replyUiState.isShowingHomepage) {
            ReplyAppContent(
                navigationType = navigationType,
                replyUiState = replyUiState,
                onTabPressed = onTabPressed,
                onEmailCardPressed = onEmailCardPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = modifier
            )
... 

接下来,让我们添加分支,以便应用能够在某些情况下显示导航栏。

  1. ReplyAppContent 可组合项主体的第一行,将 ReplyNavigationRail 可组合项包装在 AnimatedVisibility 可组合项中,并将 visible 参数设置为 true(如果 ReplyNavigationType 值为 NAVIGATION_RAIL)。

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit),
    onEmailCardPressed: (Email) -> Unit,
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier
) {
    Box(modifier = modifier) {
        AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
            ReplyNavigationRail(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
navigationItemContentList = navigationItemContentList
            )
        }
        Column(
            modifier = Modifier
                .fillMaxSize()
                .background(
                    MaterialTheme.colorScheme.inverseOnSurface
            )
        ) {
            ReplyListOnlyContent(
                replyUiState = replyUiState,
                onEmailCardPressed = onEmailCardPressed,
                modifier = Modifier.weight(1f)
                    .padding(
                        horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
                    )
            )
            ReplyBottomNavigationBar(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList,
                  modifier = Modifier
                      .fillMaxWidth()
            )
        }
    }
}     
... 
  1. 要正确对齐可组合项,请将 AnimatedVisibility 可组合项和 ReplyAppContent 主体中找到的 Column 可组合项都包装在 Row 可组合项中。

ReplyHomeScreen.kt

...
@Composable
private fun ReplyAppContent(
    navigationType: ReplyNavigationType,
    replyUiState: ReplyUiState,
    onTabPressed: ((MailboxType) -> Unit),
    onEmailCardPressed: (Email) -> Unit,
    navigationItemContentList: List<NavigationItemContent>,
    modifier: Modifier = Modifier,
) {
    Row(modifier = modifier) {
        AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
            val navigationRailContentDescription = stringResource(R.string.navigation_rail)
            ReplyNavigationRail(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList
            )
        }
        Column(
            modifier = Modifier
                .fillMaxSize()
                .background(MaterialTheme.colorScheme.inverseOnSurface)
        ) {
            ReplyListOnlyContent(
                replyUiState = replyUiState,
                onEmailCardPressed = onEmailCardPressed,
                modifier = Modifier.weight(1f)
                    .padding(
                        horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
                )
            )
            ReplyBottomNavigationBar(
                currentTab = replyUiState.currentMailbox,
                onTabPressed = onTabPressed,
                navigationItemContentList = navigationItemContentList,
                modifier = Modifier
                    .fillMaxWidth()
            )
        }
    }
}

... 

最后,让我们确保在某些情况下显示底部导航。

  1. ReplyListOnlyContent 可组合项之后,将 ReplyBottomNavigationBar 可组合项包装在 AnimatedVisibility 可组合项中。
  2. ReplyNavigationType 值为 BOTTOM_NAVIGATION 时,设置 visible 参数。

ReplyHomeScreen.kt

...
ReplyListOnlyContent(
    replyUiState = replyUiState,
    onEmailCardPressed = onEmailCardPressed,
    modifier = Modifier.weight(1f)
        .padding(
            horizontal = dimensionResource(R.dimen.email_list_only_horizontal_padding)
        )

)
AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
    val bottomNavigationContentDescription = stringResource(R.string.navigation_bottom)
    ReplyBottomNavigationBar(
        currentTab = replyUiState.currentMailbox,
        onTabPressed = onTabPressed,
        navigationItemContentList = navigationItemContentList,
        modifier = Modifier
            .fillMaxWidth()
    )
}

... 
  1. 在**展开式折叠**模式下运行应用。您应该会看到以下屏幕

bfacf9c20a30b06b.png

8. 获取解决方案代码

要下载完成的 codelab 的代码,您可以使用以下 git 命令

git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-reply-app.git 
cd basic-android-kotlin-compose-training-reply-app
git checkout nav-update

或者,您可以将存储库下载为 zip 文件,将其解压缩,然后在 Android Studio 中打开它。

如果您想查看解决方案代码,请在 GitHub 上查看

9. 结论

恭喜!您距离使 Reply 应用能够适应所有屏幕尺寸又近了一步,方法是实现了自适应导航布局。您使用许多 Android 外形规格增强了用户体验。在下一个 codelab 中,您将通过实现自适应内容布局、测试和预览进一步提高使用自适应应用的技能。

不要忘记在社交媒体上使用 #AndroidBasics 分享您的作品!

了解更多