使用 Relay 和 Jetpack Compose 构建一个完整的应用

1. 准备工作

借助 Relay 工具包,团队不仅可以在 Figma 中设计界面组件,然后直接在 Jetpack Compose 项目中使用它们,而且无需繁琐的设计规范和冗长的 QA 周期,从而能够快速提交出色的 Android 界面。

在此 Codelab 中,您将学习如何将 Relay 界面软件包集成到 Compose 开发流程中。具体内容侧重于集成技术,而非端到端工作流程。要想了解 Relay 的常规工作流程,请参阅 Relay 基本教程

前提条件

  • 有使用 Compose 的基本经验。如果您没有此类经验,请先完成 Jetpack Compose 基础知识 Codelab。
  • 有使用 Kotlin 语法的经验。

学习内容

  • 如何导入界面软件包。
  • 如何将界面软件包同导航和数据架构集成在一起。
  • 如何使用控制器逻辑来封装界面软件包。
  • 如何将 Figma 样式映射到 Compose 主题。
  • 如何在生成的代码中将界面软件包替换为现有的可组合函数。

构建内容

  • 基于设计师提供的 Relay 软件包实现的真实应用设计。该应用称为 Reflect,是一款有助于培养正念和良好习惯的日常跟踪应用。它包含一系列不同类型的跟踪器,并且具有用于添加和管理这些跟踪器的界面。该应用如下图所示:

完成后的应用

所需条件

2. 进行设置

获取代码

如需获取此 Codelab 的代码,请执行以下操作之一:

$ git clone https://github.com/googlecodelabs/relay-codelabs
  • 找到 GitHub 上的 relay-codelabs 代码库,选择所需的分支,依次点击 Code > Download ZIP,然后解压缩下载的 ZIP 文件。

无论您执行的是哪个操作,main 分支都会包含起始代码,end 分支则会包含解决方案代码。

安装 Relay for Android Studio 插件

如果您还没有 Relay for Android Studio 插件,请按以下步骤操作:

  1. 在 Android Studio 中,依次点击 Settings > Plugins
  2. 在文本框中输入 Relay for Android Studio
  3. 在搜索结果中显示的扩展程序上,点击 Install

Android Studio 插件设置

  1. 如果您看到 Third-party plugins privacy note 对话框,请点击 Accept
  2. 依次点击 OK > Restart
  3. 如果您看到 Confirm exit 对话框,请点击 Exit

将 Android Studio 关联到 Figma

Relay 需要使用 Figma API 来检索界面软件包。为了能够使用该 API,您需要具有免费 Figma 帐号个人访问令牌,因此它们列在了所需条件部分中。

如果您还没有将 Android Studio 关联到 Figma,请按以下步骤操作:

  1. 在您的 Figma 帐号中,点击页面顶部的个人资料图标,然后选择 Settings
  2. 找到 Personal access tokens 部分,在相应文本框中输入令牌说明,然后按 Enter(如果使用的是 macOS 设备,请按 return)。这会生成令牌。
  3. 点击 Copy this token

在 Figma 中生成的访问令牌

  1. 在 Android Studio 中,依次选择 Tools > Relay Settings。系统随即会显示 Relay settings 对话框。
  2. Figma Access Token 文本框中,粘贴访问令牌,然后点击 OK。这样,您的环境就设置好了。

3. 查看应用设计

对于 Reflect 应用,我们和一位设计师进行了通力合作,这位设计师帮助我们确定了该应用的颜色、排版、布局和行为。我们按照 Material Design 3 规范创作了这些设计,因此该应用可与 Material 组件和主题无缝协作。

查看主屏幕

主屏幕显示用户定义的一系列跟踪器,并提供用于更改活跃日期和创建其他跟踪器的功能。

主屏幕

在 Figma 中,我们的设计师将该屏幕分成了多个组件,定义了它们的 API,并使用 Relay for Figma 插件将它们打包在一起。这些组件打包后,您可以将它们导入到您的 Android Studio 项目中。

主屏幕组件

查看“添加/修改”屏幕

通过“添加/修改”屏幕,用户可以添加或修改跟踪器。显示的表单因跟踪器类型而略有不同。

“添加/修改”屏幕

同样,该屏幕分为多个打包在一起的组件。

“添加/修改”屏幕中的组件

查看主题

根据 Material Design 3 令牌名称,我们将该设计的颜色和排版实现为 Figma 样式。这样可以使 Compose 主题和 Material 组件具有更好的互操作性。

Figma 样式

4. 导入界面软件包

您需要先将设计源文件上传到 Figma,然后才能将界面软件包导入到项目中。

如需获取指向 Figma 源文件的链接,请按以下步骤操作:

  1. 在 Figma 中,点击 Import file,然后选择在 Codelab 项目文件夹中找到的 ReflectDesign.fig 文件。
  2. 右键点击该文件,然后选择 Copy link。在下一部分,您将需要用到复制的链接。

a98d24b4d5ee5c34.png

将界面软件包导入到项目中

  1. 在 Android Studio 中,打开 ./CompleteAppCodelab 项目。
  2. 依次点击 File > New > Import UI Packages。系统随即会显示 Import UI Packages 对话框。
  3. Figma source URL 文本框中,粘贴您在上一部分中复制的网址。

“Import UI Packages”对话框

  1. App theme 文本框中,输入 com.google.relay.example.reflect.ui.theme.ReflectTheme。这可确保生成的预览使用自定义主题。
  2. 点击 Next。您会看到文件的界面软件包的预览。
  3. 点击 Create。软件包随即会导入到您的项目中。
  4. 点击 Project 标签页,然后点击 ui-packages 文件夹旁边的 2158ffa7379d2b2e.png 展开箭头。

ui-packages 文件夹

  1. 点击任一软件包文件夹旁边的 2158ffa7379d2b2e.png 展开箭头,您会注意到文件夹中包含 JSON 源文件和关联素材资源。
  2. 打开 JSON 源文件。Relay 模块会显示软件包及其 API 的预览。

Relay 软件包预览模块

构建和生成代码

  1. 在 Android Studio 顶部,点击 b3bc77f3c78cac1b.png Make project。针对每个软件包生成的代码都会添加到 java/com.google.relay.example.reflect 文件。生成的可组合函数包含 Figma 设计中的所有布局和样式信息。
  2. 如有必要,请点击 Split,以便查看代码,以及连续显示的预览窗格。
  3. 打开 range/Range.kt 文件,您会注意到系统为每个组件变体都创建了 Compose 预览。

c0d21ab0622ad550.png

5. 集成组件

在这一部分,您将更详细地了解为 Switch 跟踪器生成的代码。

Switch 跟踪器的设计

  1. 在 Android Studio 中,打开 com/google/relay/example/reflect/switch/Switch.kt 文件。

Switch.kt(生成的)

/**
 * This composable was generated from the switch UI Package.
 * Generated code; don't edit directly.
 */
@Composable
fun Switch(
    modifier: Modifier = Modifier,
    isChecked: Boolean = false,
    isPressed: Boolean = false,
    emoji: String = "",
    title: String = ""
) {
    TopLevel(modifier = modifier) {
        if (isChecked) {
            ActiveOverlay(modifier = Modifier.rowWeight(1.0f).columnWeight(1.0f)) {}
        }
        if (isPressed) {
            State(modifier = Modifier.rowWeight(1.0f).columnWeight(1.0f)) {}
        }
        TopLevelSynth {
            Label(modifier = Modifier.rowWeight(1.0f)) {
                Emoji(emoji = emoji)
                Title(
                    title = title,
                    modifier = Modifier.rowWeight(1.0f)
                )
            }
            if (isChecked) {
                Checkmark {
                    Vector(modifier = Modifier.rowWeight(1.0f).columnWeight(1.0f))
                }
            }
        }
    }
}
  1. 请注意以下几点:
  • Figma 设计中的所有布局和样式都是生成的。
  • 子组件会分为多个单独的可组合函数。
  • 系统会为所有设计变体生成可组合函数预览。
  • 颜色和排版样式是硬编码的。您将在后面进行更正。

插入跟踪器

  1. 在 Android Studio 中,打开 java/com/google/relay/example/reflect/ui/components/TrackerControl.kt 文件。此文件为习惯跟踪器提供数据和互动逻辑。目前,该组件可输出跟踪器模型中的原始数据。

7850337c9ba23fd5.png

  1. com.google.relay.example.reflect.switch.Switch 软件包导入到文件中。
  2. 创建在 trackerData.tracker.type 字段中转换的 when 块。
  3. when 块的主体部分,当类型为 TrackerType.BOOLEAN 时,调用 Switch() Composable 函数。

您的代码应如下所示:

TrackerControl.kt

when (trackerData.tracker.type) {
    TrackerType.BOOLEAN ->
        Switch(
          title = trackerData.tracker.name,
          emoji = trackerData.tracker.emoji
        )
    else ->
        Text(trackerData.tracker.toString())
}
  1. 重新构建项目。现在,首页会按照设计使用实时数据正确呈现 Switch 跟踪器。

f07eda1a7740129b.png

6. 添加状态和互动

界面软件包是无状态的。呈现的内容是根据传入的参数生成的简单结果。但是,真实应用需要互动和状态。互动处理程序可以像任何其他参数一样传入到生成的可组合函数中,但是在哪里保留这些处理程序操控的状态呢?如何避免将同一处理程序传递到每个实例?如何将软件包的组合抽象为可重复使用的可组合函数?对于这些情况,我们建议您将生成的软件包封装在自定义 Composable 函数中。

将界面软件包封装在控制器 Composable 函数中

通过将界面软件包封装在控制器 Composable 函数中,您可以自定义呈现逻辑或业务逻辑,并在必要时管理本地状态。设计师仍可以在 Figma 中随意更新原始界面软件包,而不需要您更新封装容器代码。

如需创建 Switch 跟踪器的控制器,请按以下步骤操作:

  1. 在 Android Studio 中,打开 java/com/google/relay/example/reflect/ui/components/SwitchControl.kt 文件。
  2. SwitchControl() Composable 函数中,传入以下参数:
  • trackerData:一个 TrackerData 对象
  • modifier:一个装饰器对象
  • onLongClick:一个用于启用以下功能的互动回调:长按跟踪器即可开始进行编辑和删除操作

modifier

  1. combinedClickable 修饰符传递到 Switch() 函数,以便处理点击和长按操作。
  2. TrackerData 对象中的值(包括 isToggled() 方法)传递到 Switch() 函数。

完成后的 SwitchControl() 函数如以下代码段所示:

SwitchControl.kt

package com.google.relay.example.reflect.ui.components

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.relay.example.reflect.model.Tracker
import com.google.relay.example.reflect.model.TrackerData
import com.google.relay.example.reflect.model.TrackerType
import com.google.relay.example.reflect.switch.Switch

/*
 * A component for controlling switch-type trackers.
 *
 * SwitchControl is responsible for providing interaction and state management to the stateless
 * composable [Switch] generated by Relay. [onLongClick] provides a way for callers to supplement
 * the control's intrinsic interactions with, for example, a context menu.
 */
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SwitchControl(
    trackerData: TrackerData,
    modifier: Modifier = Modifier,
    onLongClick: (() -> Unit)? = null,
) {
    Switch(
        modifier
            .height(64.dp)
            .clip(shape = RoundedCornerShape(size = 32.dp))
            .combinedClickable(onLongClick = onLongClick) {
                trackerData.toggle()
            },
        emoji = trackerData.tracker.emoji,
        title = trackerData.tracker.name,
        isChecked = trackerData.isToggled(),
    )
}

@Preview
@Composable
fun SwitchControllerPreview() {
    val data = TrackerData(
        Tracker(
            emoji = "🍕",
            name = "Ate Pizza",
            type = TrackerType.BOOLEAN
        )
    )
    SwitchControl(data)
}
  1. TrackerControl.kt 文件中,移除 Switch 导入,然后将 Switch() 函数替换为对 SwitchControl() 函数的调用。
  2. 添加 TrackerType.RANGETrackerType.COUNT 枚举器常量对应的情况。

完成后的 when 块如以下代码段所示:

SwitchControl.kt

when (trackerData.tracker.type) {
    TrackerType.BOOLEAN ->
        SwitchControl(
            trackerData = trackerData,
            onLongClick = { expanded = true },
        )
    TrackerType.RANGE ->
        RangeControl(
            trackerData = trackerData,
            onLongClick = { expanded = true },
        )
    TrackerType.COUNT ->
        ValueControl(
            trackerData = trackerData,
            onLongClick = { expanded = true },
        )
}
  1. 重新构建项目。现在,您可以显示跟踪器并与之互动。主屏幕已完成。

b23b94f0034243d3.png

7. 映射现有组件

通过 Relay,开发者可以将界面软件包替换为现有的可组合函数,从而自定义生成的代码。这是一个在代码中输出开箱即用组件(甚至是自定义设计系统)的好方法。

映射文本字段

下图是添加/删除跟踪器对话框中的 Switch Tracker Editor 设计:

Switch 设置组件的设计

我们的设计师在设计中使用了 ReflectTextField,而对于该字段,我们基于 Material Design 3 文本字段构建的代码中已有一个实现。Figma 不提供文本字段原生支持,因此 Relay 生成的默认代码只是看起来与该设计很像,但并不是能够使用的控件。

如需替代此元素的实际实现,您需要两样东西:文本字段界面软件包和映射文件。幸运的是,我们的设计师已在 Figma 中打包设计系统组件,并在 Tracker Editor 的设计中使用了按钮组件。默认情况下,嵌套软件包是作为设置栏软件包的依赖项生成的,但是您可以使用组件映射来掉换它。

文本字段的 Figma 组件(叠加了 Relay 插件)

创建映射文件

Relay for Android Studio 插件提供用于创建组件映射文件的快捷方式。

如需创建映射文件,请按以下步骤操作:

  1. 在 Android Studio 中,右键点击 text_field 界面软件包,然后选择 Generate mapping file

“Generate mapping file”上下文菜单项

  1. 在文件中,输入以下代码:

text_field.json

{
  "target": "ReflectTextField",
  "package": "com.google.relay.example.reflect.ui.components",
  "generatePreviews": false
}

组件映射文件可识别 Compose 类目标和软件包,以及一系列可选的 fieldMapping 对象。通过这些字段映射,您可以将软件包参数转换为所需的 Compose 参数。在本示例中,API 完全相同,因此您只需指定目标类。

  1. 重新构建项目。
  2. trackersettings/ TrackerSettings.kt 文件中,找到生成的 TitleFieldStyleFilledStateEnabledTextConfigurationsInputText() 可组合函数,您会注意到,其中包含生成的 ReflectTextField 组件。

TrackerSettings.kt(生成的)

@Composable
fun TitleFieldStyleFilledStateEnabledTextConfigurationsInputText(
    onTitleChanged: (String) -> Unit,
    title: String,
    modifier: Modifier = Modifier
) {
    ReflectTextField(
        onChange = onTitleChanged,
        labelText = "Title",
        leadingIcon = "search",
        trailingIcon = "cancel",
        supportingText = "Supporting text",
        inputText = title,
        state = State.Enabled,
        textConfigurations = TextConfigurations.InputText,
        modifier = modifier.requiredHeight(56.0.dp)
    )
}

8. 映射到 Compose 主题

默认情况下,Relay 会为颜色和排版生成字面量值。这可确保转换准确性,但会导致组件无法使用 Compose 主题系统。在您以深色模式查看应用时,这会非常明显:

使用深色模式且显示错误颜色的主屏幕的预览

日期导航组件几乎不可见,并且颜色是错误的。要想解决这个问题,您可以使用 Relay 中的样式映射功能,将 Figma 样式关联到生成的代码中的 Compose 主题令牌。这可以提高 Relay 与 Material Design 3 组件之间的外观一致性,并实现深色模式支持。

1fac916db14929bb.png

创建样式映射文件

  1. 在 Android Studio 中,找到 src/main/ui-package-resources/style-mappings 文件夹,然后创建包含以下代码的 figma_styles.json 文件:

figma_styles.json

{
  "figma": {
    "colors": {
      "Reflect Light/background": "md.sys.color.background",
      "Reflect Dark/background": "md.sys.color.background",
      "Reflect Light/on-background": "md.sys.color.on-background",
      "Reflect Dark/on-background": "md.sys.color.on-background",
      "Reflect Light/surface": "md.sys.color.surface",
      "Reflect Dark/surface": "md.sys.color.surface",
      "Reflect Light/on-surface": "md.sys.color.on-surface",
      "Reflect Dark/on-surface": "md.sys.color.on-surface",
      "Reflect Light/surface-variant": "md.sys.color.surface-variant",
      "Reflect Dark/surface-variant": "md.sys.color.surface-variant",
      "Reflect Light/on-surface-variant": "md.sys.color.on-surface-variant",
      "Reflect Dark/on-surface-variant": "md.sys.color.on-surface-variant",
      "Reflect Light/primary": "md.sys.color.primary",
      "Reflect Dark/primary": "md.sys.color.primary",
      "Reflect Light/on-primary": "md.sys.color.on-primary",
      "Reflect Dark/on-primary": "md.sys.color.on-primary",
      "Reflect Light/primary-container": "md.sys.color.primary-container",
      "Reflect Dark/primary-container": "md.sys.color.primary-container",
      "Reflect Light/on-primary-container": "md.sys.color.on-primary-container",
      "Reflect Dark/on-primary-container": "md.sys.color.on-primary-container",
      "Reflect Light/secondary-container": "md.sys.color.secondary-container",
      "Reflect Dark/secondary-container": "md.sys.color.secondary-container",
      "Reflect Light/on-secondary-container": "md.sys.color.on-secondary-container",
      "Reflect Dark/on-secondary-container": "md.sys.color.on-secondary-container",
      "Reflect Light/outline": "md.sys.color.outline",
      "Reflect Dark/outline": "md.sys.color.outline",
      "Reflect Light/error": "md.sys.color.error",
      "Reflect Dark/error": "md.sys.color.error"
    },
    "typography": {
      "symbols": {
        "Reflect/headline/large": "md.sys.typescale.headline-large",
        "Reflect/headline/medium": "md.sys.typescale.headline-medium",
        "Reflect/headline/small": "md.sys.typescale.headline-small",
        "Reflect/title/large": "md.sys.typescale.title-large",
        "Reflect/title/medium": "md.sys.typescale.title-medium",
        "Reflect/title/small": "md.sys.typescale.title-small",
        "Reflect/body/large": "md.sys.typescale.body-large",
        "Reflect/body/medium": "md.sys.typescale.body-medium",
        "Reflect/body/small": "md.sys.typescale.body-small",
        "Reflect/label/large": "md.sys.typescale.label-large",
        "Reflect/label/medium": "md.sys.typescale.label-medium",
        "Reflect/label/small": "md.sys.typescale.label-small"
      },
      "subproperties": {
        "fontFamily": "font",
        "fontWeight": "weight",
        "fontSize": "size",
        "letterSpacing": "tracking",
        "lineHeightPx": "line-height"
      }
    }
  },
  "compose": {
    "colors": {
      "md.sys.color.background": "MaterialTheme.colorScheme.background",
      "md.sys.color.error": "MaterialTheme.colorScheme.error",
      "md.sys.color.error-container": "MaterialTheme.colorScheme.errorContainer",
      "md.sys.color.inverse-on-surface": "MaterialTheme.colorScheme.inverseOnSurface",
      "md.sys.color.inverse-surface": "MaterialTheme.colorScheme.inverseSurface",
      "md.sys.color.on-background": "MaterialTheme.colorScheme.onBackground",
      "md.sys.color.on-error": "MaterialTheme.colorScheme.onError",
      "md.sys.color.on-error-container": "MaterialTheme.colorScheme.onErrorContainer",
      "md.sys.color.on-primary": "MaterialTheme.colorScheme.onPrimary",
      "md.sys.color.on-primary-container": "MaterialTheme.colorScheme.onPrimaryContainer",
      "md.sys.color.on-secondary": "MaterialTheme.colorScheme.onSecondary",
      "md.sys.color.on-secondary-container": "MaterialTheme.colorScheme.onSecondaryContainer",
      "md.sys.color.on-surface": "MaterialTheme.colorScheme.onSurface",
      "md.sys.color.on-surface-variant": "MaterialTheme.colorScheme.onSurfaceVariant",
      "md.sys.color.on-tertiary": "MaterialTheme.colorScheme.onTertiary",
      "md.sys.color.on-tertiary-container": "MaterialTheme.colorScheme.onTertiaryContainer",
      "md.sys.color.outline": "MaterialTheme.colorScheme.outline",
      "md.sys.color.primary": "MaterialTheme.colorScheme.primary",
      "md.sys.color.primary-container": "MaterialTheme.colorScheme.primaryContainer",
      "md.sys.color.secondary": "MaterialTheme.colorScheme.secondary",
      "md.sys.color.secondary-container": "MaterialTheme.colorScheme.secondaryContainer",
      "md.sys.color.surface": "MaterialTheme.colorScheme.surface",
      "md.sys.color.surface-variant": "MaterialTheme.colorScheme.surfaceVariant",
      "md.sys.color.tertiary": "MaterialTheme.colorScheme.tertiary",
      "md.sys.color.tertiary-container": "MaterialTheme.colorScheme.tertiaryContainer"
    },
    "typography": {
      "symbols": {
        "md.sys.typescale.display-large": "MaterialTheme.typography.displayLarge",
        "md.sys.typescale.display-medium": "MaterialTheme.typography.displayMedium",
        "md.sys.typescale.display-small": "MaterialTheme.typography.displaySmall",
        "md.sys.typescale.headline-large": "MaterialTheme.typography.headlineLarge",
        "md.sys.typescale.headline-medium": "MaterialTheme.typography.headlineMedium",
        "md.sys.typescale.headline-small": "MaterialTheme.typography.headlineSmall",
        "md.sys.typescale.title-large": "MaterialTheme.typography.titleLarge",
        "md.sys.typescale.title-medium": "MaterialTheme.typography.titleMedium",
        "md.sys.typescale.title-small": "MaterialTheme.typography.titleSmall",
        "md.sys.typescale.body-large": "MaterialTheme.typography.bodyLarge",
        "md.sys.typescale.body-medium": "MaterialTheme.typography.bodyMedium",
        "md.sys.typescale.body-small": "MaterialTheme.typography.bodySmall",
        "md.sys.typescale.label-large": "MaterialTheme.typography.labelLarge",
        "md.sys.typescale.label-medium": "MaterialTheme.typography.labelMedium",
        "md.sys.typescale.label-small": "MaterialTheme.typography.labelSmall"
      },
      "subproperties": {
        "font": "fontFamily",
        "weight": "fontWeight",
        "size": "fontSize",
        "tracking": "letterSpacing",
        "line-height": "lineHeight"
      }
    },
    "options": {
      "packages": {
        "MaterialTheme": "androidx.compose.material3"
      }
    }
  }
}

主题映射文件由以下两个顶级对象组成:figmacompose。在这些对象中,两个环境之间的颜色和类型定义通过中级令牌关联起来。这样一来,多个 Figma 样式可以映射到单个 Compose 主题条目,这在您支持浅色主题和深色主题时非常有用。

  1. 查看映射文件,尤其是该文件如何将排版属性从 Figma 重新映射到 Compose 所需的主题。

重新导入界面软件包

创建映射文件后,您需要将所有界面软件包重新导入到项目中,这是因为在初始导入时,由于尚未提供映射文件,因此舍弃了所有 Figma 样式值。

如需重新导入界面软件包,请按以下步骤操作:

  1. 在 Android Studio 中,依次点击 File > New > Import UI Packages。系统随即会显示 Import UI Packages 对话框。
  2. Figma source URL 文本框中,输入 Figma 源文件的网址。
  3. 选中 Translate Figma styles to Compose theme 复选框。
  4. 点击 Next。您会看到文件的界面软件包的预览。
  5. 点击 Create。软件包随即会导入到您的项目中。

“Import UI Packages”对话框

  1. 重新构建项目,然后打开 switch/Switch.kt 文件以查看生成的代码。

Switch.kt(生成的)

@Composable
fun ActiveOverlay(
    modifier: Modifier = Modifier,
    content: @Composable RelayContainerScope.() -> Unit
) {
    RelayContainer(
        backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
        isStructured = false,
        radius = 32.0,
        content = content,
        modifier = modifier.fillMaxWidth(1.0f).fillMaxHeight(1.0f)
    )
}
  1. 请注意在 Compose 主题对象中,backgroundColor 参数是如何设置为 MaterialTheme.colorScheme.surfaceVariant 字段的。
  2. 在预览窗格中,将应用切换为深色模式。该主题会正确应用,并且外观 bug 会得到修复。

6cf2aa19fabee292.png

9. 恭喜

恭喜!您已学习如何将 Relay 集成到 Compose 应用中!

了解详情