1. Before you begin
Relay is a toolkit that lets teams design UI components in Figma and use them directly in Jetpack Compose projects. It removes the need for tedious design specification and QA cycles, which helps teams quickly deliver great Android UIs.
In this codelab, you learn how to integrate Relay UI Packages into your Compose development process. It focuses on integration techniques, not the end-to-end workflow. To learn about the general workflow for Relay, see the Relay basic tutorial.
Prerequisites
- Basic experience with Compose. If you haven't already done so, complete the Jetpack Compose basics codelab.
- Experience with Kotlin syntax.
What you'll learn
- How to import UI Packages.
- How to integrate UI Packages with navigation and data architecture.
- How to wrap UI Packages with controller logic.
- How to map Figma styles to your Compose theme.
- How to replace UI Packages with existing composables in generated code.
What you'll build
- A realistic app design based on Relay packages provided by a designer. The app is called Reflect, a daily tracking app that promotes mindfulness and good habits. It contains a collection of trackers of various types and UI to add and manage them. The app looks like the following image:
What you'll need
- Android Studio Electric Eel or later
- A free Figma account and personal access token
2. Get set up
Get the code
To get the code for this codelab, do one of the following:
- Clone the
relay-codelabs
repository from GitHub:
$ git clone https://github.com/googlecodelabs/relay-codelabs
- Navigate to the
relay-codelabs
repository on GitHub, select the desired branch, and then click Code > Download zip and unpack the downloaded zip file.
In either case, the main
branch contains the starter code and the end
branch contains the solution code.
Install the Relay for Android Studio plugin
If you don't already have the Relay for Android Studio plugin, follow these steps:
- In Android Studio, click Settings > Plugins.
- In the text box, enter
Relay for Android Studio
. - On the extension that appears in the search results, click Install.
- If you see a Third-party plugins privacy note dialog, click Accept.
- Click OK > Restart.
- If you see a Confirm exit dialog, click Exit.
Connect Android Studio to Figma
Relay retrieves UI Packages with the Figma API. To use it, you need a free Figma account and a personal access token, hence why they're listed in the What you'll need section.
If you haven't already connected Android Studio to Figma, follow these steps:
- In your Figma account, click your profile icon at the top of the page and select Settings.
- In the Personal access tokens section, enter a description for the token in the text box and then press
Enter
(orreturn
on macOS). A token is generated. - Click Copy this token.
- In Android Studio, select Tools > Relay Settings. A Relay settings dialog appears.
- In the Figma Access Token text box, paste the access token and then click OK. Your environment is set up.
3. Review the app's design
For the Reflect app, we worked with a designer to help us define the app's color, typography, layout, and behavior. We created these designs in accordance with Material Design 3 conventions so that the app works seamlessly with Material components and themes.
Review the home screen
The home screen displays a list of user-defined trackers. It also provides affordances to change the active day and create other trackers.
In Figma, our designer divided this screen into multiple components, defined their APIs, and packaged them with the Relay for Figma plugin. After these components are packaged, you can import them into your Android Studio project.
Review the add/edit screen
The add/edit screen lets users add or edit trackers. The form displayed is slightly different based on tracker type.
Similarly, this screen is divided into multiple packaged components.
Review the theme
Colors and typography for this design are implemented as Figma styles based on Material Design 3 token names. This provides better interoperability with Compose themes and Material components.
4. Import UI Packages
Get the link to the Figma source
Before you can import UI Packages into your project, you need to upload the design source to Figma.
To get the link to the Figma source, follow these steps:
- In Figma, click Import file and then select the
ReflectDesign.fig
file found in theCompleteAppCodelab
project folder. - Right-click the file and then select Copy link. You need it in the next section.
Import UI Packages into the project
- In Android Studio, open the
./CompleteAppCodelab
project. - Click File > New > Import UI Packages. An Import UI Packages dialog appears.
- In the Figma source URL text box, paste the URL that you copied in the previous section.
- In the App theme text box, enter
com.google.relay.example.reflect.ui.theme.ReflectTheme
. This ensures that generated previews use the custom theme. - Click Next. You see a preview of the file's UI Packages.
- Click Create. The packages are imported into your project.
- Navigate to the Project tab and then click the expander arrow next to the
ui-packages
folder.
- Click the expander arrow next to one of the package folders, and then notice that it contains a
JSON
source file and asset dependencies. - Open the
JSON
source file. The Relay module displays a preview of the package and its API.
Build and generate code
- At the top of Android Studio, click Make project. Generated code for each package is added to the
java/com.google.relay.example.reflect
folder. Generated composables contain all layout and styling information from the Figma design. - Open the
com/google/relay/example/reflect/range/Range.kt
file. - Notice that Compose previews are created for each component variation. If necessary, click Split so that you see the code and preview panes next to each other.
5. Integrate components
In this section, you take a closer look at the generated code for the Switch tracker.
- In Android Studio, open the
com/google/relay/example/reflect/switch/Switch.kt
file.
Switch.kt (generated)
/**
* This composable was generated from the UI Package 'switch'.
* Generated code; don't edit directly.
*/
@Composable
fun Switch(
modifier: Modifier = Modifier,
isChecked: Boolean = false,
emoji: String = "",
title: String = ""
) {
TopLevel(modifier = modifier) {
if (isChecked) {
ActiveOverlay(modifier = Modifier.rowWeight(1.0f).columnWeight(1.0f)) {}
}
TopLevelSynth(modifier = Modifier.rowWeight(1.0f)) {
Label(modifier = Modifier.rowWeight(1.0f)) {
Emoji(emoji = emoji)
Title(
title = title,
modifier = Modifier.rowWeight(1.0f)
)
}
Checkmark {
if (isChecked) {
Icon()
}
}
}
}
}
- Notice the following:
- All layout and styling from the Figma design is generated.
- Sub-elements are divided into separate composables.
- Composable Previews are generated for all design variations.
- Color and typography styles are hardcoded. You fix this later.
Insert the tracker
- In Android Studio, open the
java/com/google/relay/example/reflect/ui/components/TrackerControl.kt
file. This file provides data and interaction logic to habit trackers. - Build and run the app in the emulator. Currently, this component outputs raw data from the tracker model.
- Import the
com.google.relay.example.reflect.switch.Switch
package into the file. - Replace
Text(text = trackerData.tracker.toString())
with awhen
block that pivots on thetrackerData.tracker.type
field. - In the body of the
when
block, call theSwitch()
Composable
function when the type isTrackerType.BOOLEAN
.
Your code should look like this:
TrackerControl.kt
// TODO: replace with Relay tracker components
when (trackerData.tracker.type) {
TrackerType.BOOLEAN ->
Switch(
title = trackerData.tracker.name,
emoji = trackerData.tracker.emoji
)
else ->
Text(trackerData.tracker.toString())
}
- Rebuild the project. Now the homepage correctly renders the Switch tracker as designed with live data.
6. Add state and interaction
UI Packages are stateless. What's rendered is a simple result of the parameters passed in. But real apps need interaction and state. Interaction handlers can be passed into generated composables like any other parameters, but where do you keep the state that those handlers manipulate? How do you avoid passing the same handler to every instance? How can you abstract compositions of packages into reusable composables? For these cases, we recommend that you wrap your generated packages in a custom Composable
function.
Wrap UI packages in a controller Composable
function
Wrapping UI Packages in a controller Composable
function lets you customize presentation or business logic and, if necessary, manage local state. Designers are still free to update the original UI Package in Figma without requiring you to update the wrapper code.
To create a controller for the Switch tracker, follow these steps:
- In Android Studio, open the
java/com/google/relay/example/reflect/ui/components/SwitchControl.kt
file. - In the
SwitchControl()
Composable
function, pass in the following parameters:
trackerData
: aTrackerData
objectmodifier
: a decorator objectonLongClick
: an interaction callback to enable long press on trackers for edit and delete
- Insert a
Switch()
function and pass in acombinedClickable
modifier to handle click and long press. - Pass values from the
TrackerData
object to theSwitch()
function, including theisToggled()
method.
The completed SwitchControl()
function looks like this code snippet:
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
.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)
}
- In the
TrackerControl.kt
file, remove theSwitch
import and then replace theSwitch()
function with a call to theSwitchControl()
function. - Add cases for the
TrackerType.RANGE
andTrackerType.COUNT
enumerator constants.
The completed when
block looks like this code snippet:
TrackerControl.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 },
)
}
- Rebuild the project. Now you can display and interact with trackers. The home screen is complete.
7. Map existing components
Relay lets developers customize generated code by replacing UI Packages with existing composables. This is a great way to output out-of-the-box components or even custom design systems in your code.
Map a text field
The following image is the design for the Tracker Settings
component in the Add/edit tracker dialog:
Our designer used a ReflectTextField
in the design, for which we already have an implementation in code built on top of Material Design 3 text fields. Figma doesn't support text fields natively, so the default code generated by Relay only looks like the design; it's not a functional control.
To test out the current implementation for TrackerSettings
:
- In Android Studio, build and run the app in the emulator.
- Long press on a tracker row and select Edit.
- Tap on the
Title
text field and note that it doesn't respond to interaction.
To substitute the real implementation for this element, you need two things: a text field UI package and a mapping file. Fortunately our designer already packaged our design system components in Figma and used a text field component in their design for Tracker Settings
. By default, this nested package is generated as a dependency, but you use component mapping to swap it.
Create a mapping file
The Relay for Android Studio plugin provides a shortcut to create component mapping files.
To create a mapping file, follow these steps:
- In Android Studio, right-click the
text_field
UI package and then select Generate mapping file.
- A mapping file dialog will show. Enter the following options:
- In Target composable, select Use existing composable and enter
com.google.relay.example.reflect.ui.components.ReflectTextField
- In Generated file, check Generate implementation and uncheck Generate Compose Preview
- Click Generate mapping file. This will generate the following mapping file:
text_field.json
{
"target": "ReflectTextField",
"package": "com.google.relay.example.reflect.ui.components",
"generateImplementation": true,
"generatePreviews": false,
}
Component-mapping files identify a Compose class target and package, and an optional collection of fieldMapping
objects. These field mappings let you transform package parameters into the expected Compose parameters. In this case, the APIs are identical, so you only need to specify the target class.
- Rebuild the project.
- In the
trackersettings/
TrackerSettings.kt
file, find the generatedTitleFieldStyleFilledStateEnabledTextConfigurationsInputText()
Composable function and note that it includes a generatedReflectTextField
component.
TrackerSettings.kt (generated)
@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.fillMaxWidth(1.0f).requiredHeight(56.0.dp)
)
}
- Rebuild the project. Now you can interact with tracker settings fields. The edit screen is complete.
8. Map to Compose themes
By default, Relay generates literal values for colors and typography. This ensures translation accuracy, but prevents components from using the Compose theming system. This is obvious when you view your app in dark mode:
The day navigation component is almost invisible and the colors are wrong. To fix this, you use the style-mapping feature in Relay to link Figma styles to Compose theme tokens in your generated code. This increases visual consistency between Relay and Material Design 3 components, and enables dark mode support.
Create a style-mapping file
- In Android Studio, navigate to the
src/main/ui-package-resources
directory and create a new directory namedstyle-mappings
. In that directory, create afigma_styles.json
file that contains the following code:
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"
}
}
}
}
Theme mapping files are structured with two top-level objects: figma
and compose
. Inside these objects, color and type definitions are linked between both environments through intermediate tokens. This allows multiple Figma styles to map to a single Compose theme entry, which is useful when you support light and dark themes.
- Review the mapping file, especially how it remaps typography properties from Figma to what Compose expects.
Reimport UI Packages
After you create a mapping file, you need to reimport all UI Packages into your project because all Figma style values were discarded upon initial import since a mapping file wasn't provided.
To reimport UI Packages, follow these steps:
- In Android Studio, click File > New > Import UI Packages. An Import UI Packages dialog appears.
- In the Figma source URL text box, enter the URL of the Figma source file.
- Select the Translate Figma styles to Compose theme checkbox.
- Select Import custom configuration. Click the folder icon then select the file you just created:
src/main/ui-package-resources/style-mappings/figma_styles.json
. - Click Next. You see a preview of the file's UI Packages.
- Click Create. The packages are imported into your project.
- Rebuild your project and then open the
switch/Switch.kt
file to view the generated code.
Switch.kt (generated)
@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)
)
}
- Notice how the
backgroundColor
parameter is set to theMaterialTheme.colorScheme.surfaceVariant
field in the Compose theme object. - Run the project and switch on dark mode in the emulator. The theme is correctly applied and the visual bugs are fixed.
9. Congratulations
Congratulations! You learned how to integrate Relay into your Compose apps!