1. 准备工作
借助 Relay 工具包,团队不仅可以在 Figma 中设计界面组件,然后直接在 Jetpack Compose 项目中使用它们,而且无需繁琐的设计规范和冗长的 QA 周期,从而能够快速提交出色的 Android 界面。
在此 Codelab 中,您将学习如何将 Relay 界面软件包集成到 Compose 开发流程中。具体内容侧重于集成技术,而非端到端工作流程。要想了解 Relay 的常规工作流程,请参阅 Relay 基本教程。
前提条件
- 有使用 Compose 的基本经验。如果您没有此类经验,请先完成 Jetpack Compose 基础知识 Codelab。
- 有使用 Kotlin 语法的经验。
学习内容
- 如何导入界面软件包。
- 如何将界面软件包同导航和数据架构集成在一起。
- 如何使用控制器逻辑来封装界面软件包。
- 如何将 Figma 样式映射到 Compose 主题。
- 如何在生成的代码中将界面软件包替换为现有的可组合函数。
构建内容
- 基于设计师提供的 Relay 软件包实现的真实应用设计。该应用称为 Reflect,是一款有助于培养正念和良好习惯的日常跟踪应用。它包含一系列不同类型的跟踪器,并且具有用于添加和管理这些跟踪器的界面。该应用如下图所示:
所需条件
- 最新版本的 Android Studio
- 免费 Figma 帐号和个人访问令牌
2. 进行设置
获取代码
如需获取此 Codelab 的代码,请执行以下操作之一:
- 从 GitHub 克隆
relay-codelabs
代码库:
$ 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 插件,请按以下步骤操作:
- 在 Android Studio 中,依次点击 Settings > Plugins。
- 在文本框中输入
Relay for Android Studio
。 - 在搜索结果中显示的扩展程序上,点击 Install。
- 如果您看到 Third-party plugins privacy note 对话框,请点击 Accept。
- 依次点击 OK > Restart。
- 如果您看到 Confirm exit 对话框,请点击 Exit。
将 Android Studio 关联到 Figma
Relay 需要使用 Figma API 来检索界面软件包。为了能够使用该 API,您需要具有免费 Figma 帐号和个人访问令牌,因此它们列在了所需条件部分中。
如果您还没有将 Android Studio 关联到 Figma,请按以下步骤操作:
- 在您的 Figma 帐号中,点击页面顶部的个人资料图标,然后选择 Settings。
- 找到 Personal access tokens 部分,在相应文本框中输入令牌说明,然后按
Enter
(如果使用的是 macOS 设备,请按return
)。这会生成令牌。 - 点击 Copy this token。
- 在 Android Studio 中,依次选择 Tools > Relay Settings。系统随即会显示 Relay settings 对话框。
- 在 Figma Access Token 文本框中,粘贴访问令牌,然后点击 OK。这样,您的环境就设置好了。
3. 查看应用设计
对于 Reflect 应用,我们和一位设计师进行了通力合作,这位设计师帮助我们确定了该应用的颜色、排版、布局和行为。我们按照 Material Design 3 规范创作了这些设计,因此该应用可与 Material 组件和主题无缝协作。
查看主屏幕
主屏幕显示用户定义的一系列跟踪器,并提供用于更改活跃日期和创建其他跟踪器的功能。
在 Figma 中,我们的设计师将该屏幕分成了多个组件,定义了它们的 API,并使用 Relay for Figma 插件将它们打包在一起。这些组件打包后,您可以将它们导入到您的 Android Studio 项目中。
查看“添加/修改”屏幕
通过“添加/修改”屏幕,用户可以添加或修改跟踪器。显示的表单因跟踪器类型而略有不同。
同样,该屏幕分为多个打包在一起的组件。
查看主题
根据 Material Design 3 令牌名称,我们将该设计的颜色和排版实现为 Figma 样式。这样可以使 Compose 主题和 Material 组件具有更好的互操作性。
4. 导入界面软件包
获取指向 Figma 源文件的链接
您需要先将设计源文件上传到 Figma,然后才能将界面软件包导入到项目中。
如需获取指向 Figma 源文件的链接,请按以下步骤操作:
- 在 Figma 中,点击 Import file,然后选择在 Codelab 项目文件夹中找到的
ReflectDesign.fig
文件。 - 右键点击该文件,然后选择 Copy link。在下一部分,您将需要用到复制的链接。
将界面软件包导入到项目中
- 在 Android Studio 中,打开
./CompleteAppCodelab
项目。 - 依次点击 File > New > Import UI Packages。系统随即会显示 Import UI Packages 对话框。
- 在 Figma source URL 文本框中,粘贴您在上一部分中复制的网址。
- 在 App theme 文本框中,输入
com.google.relay.example.reflect.ui.theme.ReflectTheme
。这可确保生成的预览使用自定义主题。 - 点击 Next。您会看到文件的界面软件包的预览。
- 点击 Create。软件包随即会导入到您的项目中。
- 点击 Project 标签页,然后点击
ui-packages
文件夹旁边的展开箭头。
- 点击任一软件包文件夹旁边的
展开箭头,您会注意到文件夹中包含
JSON
源文件和关联素材资源。 - 打开
JSON
源文件。Relay 模块会显示软件包及其 API 的预览。
构建和生成代码
- 在 Android Studio 顶部,点击
Make project。针对每个软件包生成的代码都会添加到
java/com.google.relay.example.reflect
文件。生成的可组合函数包含 Figma 设计中的所有布局和样式信息。 - 如有必要,请点击 Split,以便查看代码,以及连续显示的预览窗格。
- 打开
range/Range.kt
文件,您会注意到系统为每个组件变体都创建了 Compose 预览。
5. 集成组件
在这一部分,您将更详细地了解为 Switch 跟踪器生成的代码。
- 在 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))
}
}
}
}
}
- 请注意以下几点:
- Figma 设计中的所有布局和样式都是生成的。
- 子组件会分为多个单独的可组合函数。
- 系统会为所有设计变体生成可组合函数预览。
- 颜色和排版样式是硬编码的。您将在后面进行更正。
插入跟踪器
- 在 Android Studio 中,打开
java/com/google/relay/example/reflect/ui/components/TrackerControl.kt
文件。此文件为习惯跟踪器提供数据和互动逻辑。目前,该组件可输出跟踪器模型中的原始数据。
- 将
com.google.relay.example.reflect.switch.Switch
软件包导入到文件中。 - 创建在
trackerData.tracker.type
字段中转换的when
块。 - 在
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())
}
- 重新构建项目。现在,首页会按照设计使用实时数据正确呈现 Switch 跟踪器。
6. 添加状态和互动
界面软件包是无状态的。呈现的内容是根据传入的参数生成的简单结果。但是,真实应用需要互动和状态。互动处理程序可以像任何其他参数一样传入到生成的可组合函数中,但是在哪里保留这些处理程序操控的状态呢?如何避免将同一处理程序传递到每个实例?如何将软件包的组合抽象为可重复使用的可组合函数?对于这些情况,我们建议您将生成的软件包封装在自定义 Composable
函数中。
将界面软件包封装在控制器 Composable
函数中
通过将界面软件包封装在控制器 Composable
函数中,您可以自定义呈现逻辑或业务逻辑,并在必要时管理本地状态。设计师仍可以在 Figma 中随意更新原始界面软件包,而不需要您更新封装容器代码。
如需创建 Switch 跟踪器的控制器,请按以下步骤操作:
- 在 Android Studio 中,打开
java/com/google/relay/example/reflect/ui/components/SwitchControl.kt
文件。 - 在
SwitchControl()
Composable
函数中,传入以下参数:
trackerData
:一个TrackerData
对象modifier
:一个装饰器对象onLongClick
:一个用于启用以下功能的互动回调:长按跟踪器即可开始进行编辑和删除操作
modifier
- 将
combinedClickable
修饰符传递到Switch()
函数,以便处理点击和长按操作。 - 将
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)
}
- 在
TrackerControl.kt
文件中,移除Switch
导入,然后将Switch()
函数替换为对SwitchControl()
函数的调用。 - 添加
TrackerType.RANGE
和TrackerType.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 },
)
}
- 重新构建项目。现在,您可以显示跟踪器并与之互动。主屏幕已完成。
7. 映射现有组件
通过 Relay,开发者可以将界面软件包替换为现有的可组合函数,从而自定义生成的代码。这是一个在代码中输出开箱即用组件(甚至是自定义设计系统)的好方法。
映射文本字段
下图是添加/删除跟踪器对话框中的 Switch Tracker Editor
设计:
我们的设计师在设计中使用了 ReflectTextField
,而对于该字段,我们基于 Material Design 3 文本字段构建的代码中已有一个实现。Figma 不提供文本字段原生支持,因此 Relay 生成的默认代码只是看起来与该设计很像,但并不是能够使用的控件。
如需替代此元素的实际实现,您需要两样东西:文本字段界面软件包和映射文件。幸运的是,我们的设计师已在 Figma 中打包设计系统组件,并在 Tracker Editor
的设计中使用了按钮组件。默认情况下,嵌套软件包是作为设置栏软件包的依赖项生成的,但是您可以使用组件映射来掉换它。
创建映射文件
Relay for Android Studio 插件提供用于创建组件映射文件的快捷方式。
如需创建映射文件,请按以下步骤操作:
- 在 Android Studio 中,右键点击
text_field
界面软件包,然后选择 Generate mapping file。
- 在文件中,输入以下代码:
text_field.json
{
"target": "ReflectTextField",
"package": "com.google.relay.example.reflect.ui.components",
"generatePreviews": false
}
组件映射文件可识别 Compose 类目标和软件包,以及一系列可选的 fieldMapping
对象。通过这些字段映射,您可以将软件包参数转换为所需的 Compose 参数。在本示例中,API 完全相同,因此您只需指定目标类。
- 重新构建项目。
- 在
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 组件之间的外观一致性,并实现深色模式支持。
创建样式映射文件
- 在 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"
}
}
}
}
主题映射文件由以下两个顶级对象组成:figma
和 compose
。在这些对象中,两个环境之间的颜色和类型定义通过中级令牌关联起来。这样一来,多个 Figma 样式可以映射到单个 Compose 主题条目,这在您支持浅色主题和深色主题时非常有用。
- 查看映射文件,尤其是该文件如何将排版属性从 Figma 重新映射到 Compose 所需的主题。
重新导入界面软件包
创建映射文件后,您需要将所有界面软件包重新导入到项目中,这是因为在初始导入时,由于尚未提供映射文件,因此舍弃了所有 Figma 样式值。
如需重新导入界面软件包,请按以下步骤操作:
- 在 Android Studio 中,依次点击 File > New > Import UI Packages。系统随即会显示 Import UI Packages 对话框。
- 在 Figma source URL 文本框中,输入 Figma 源文件的网址。
- 选中 Translate Figma styles to Compose theme 复选框。
- 点击 Next。您会看到文件的界面软件包的预览。
- 点击 Create。软件包随即会导入到您的项目中。
- 重新构建项目,然后打开
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)
)
}
- 请注意在 Compose 主题对象中,
backgroundColor
参数是如何设置为MaterialTheme.colorScheme.surfaceVariant
字段的。 - 在预览窗格中,将应用切换为深色模式。该主题会正确应用,并且外观 bug 会得到修复。
9. 恭喜
恭喜!您已学习如何将 Relay 集成到 Compose 应用中!