DEV Community

Cover image for Under the hood of Relay, Android Studio plugin for exporting Figma components to compose
Kevin Schildhorn
Kevin Schildhorn

Posted on

Under the hood of Relay, Android Studio plugin for exporting Figma components to compose

Recently Google announced Relay, a new process that allows teams to create UI in Figma and generate high-fidelity Compose UI components. It's currently in open alpha, and can be installed as a plugin for Android Studio. It's a really interesting tool that automatically generates compose code based on the components you choose to export from Figma. There's a overview of what it does and how to run it here, but you may be asking: what's really going on under the hood? So let's go over in detail the process from Figma to Compose code using Relay.

Note: this is not a tutorial for Figma, rather just a look at what code is generated and how it compares to custom compose code.

Note: Relay is a new and constantly evolving library, so some of these findings may change in the future. For reference I am testing Relay version "0.3.00".

Step 1. Figma

For this blog I made an example Figma page with a simple component, a button.

Image description

This button is somewhat simple, it's a rectangular shape with a text as a subview. It has been exported as a component, and has some properties defined in its design.

Image description

If you're not familiar with Figma, that's fine. Here are the main things to point out:

The button:

  • has a radius of 15
  • Has external padding of 10
  • Is centrally aligned
  • Has a Color of Primary*

Most of these are straight forward properties, one to note is the Color, which is defined in Figma as a Color Style so it can be re-used.

The Text:

  • Fills the button
  • Has a Typography of Button*
  • Has a defined content(text)
  • Has Text Centered both horizontally and vertically
  • Has a Color of PrimaryText

Again most things are straight forward but it's worth noting the Text Style Button is defined in Figma. It contains the font name, style, and size (as well as some spacing).

Looks pretty good (I hope!), let's export it and see what we get in Android Studio.

Step 2. UI Packages

Image description

In Android Studio we've imported this button as a ui-package. Here we can see some small files like text files and a config file. We also see a font folder, containing all the variants of the font used in the button, which is interesting. By default Relay puts all the fonts into each ui-package, even if they are shared between components. In other words if you have two components(like a button and a textfield) and they both use the same custom font, then Relay will add two copies of that same font to your project rather than referencing one copy. There is an experimental feature to use shared typography but that's beyond the scope of this post.

Let's look at the json file. Here we can see everything we've defined. Each part of the component has its own name and id, and is defined in "atoms" (This seems to be in align with the concept of Atomic Design). Then in "modes" we can see all the properties from Figma.

{
  "name": "button_primary",
  "source-key": { ... },
  ...,
  "design": {
    "atoms": [
      {
        "type": "group",
        "id": "top_level",
        "root": "true"
      },
      {
        "type": "text",
        "id": "buttonText"
      }
    ],
    "modes": {
      "Button/Primary": {
        "rules": [
          {
            "id": "top_level",
            ...
          },
          {
            "id": "buttonText",
            ...
          }
        ]
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

It's a large file so I'll break it up for clarity. We can see that we have our two atoms, and an array of rules associated with the atoms.

{
  "id": "top_level",
  "padding": "10.0",
  "border-radius": "15.0",
  "main-axis-align": "start",
  "cross-axis-align": "start",
  "children": ["buttonText"],
  "item-spacing": "10.0",
  "background-color": {
    "alpha": "1.0",
    "hue": "38.82352941176471",
    "saturation": "1.0",
    "value": "1.0"
  },
  "clip-content": "false"
}
Enter fullscreen mode Exit fullscreen mode

First off we can see this layer has been renamed to "top_level", which makes sense but can seem odd. It has padding, border-radius, alignment, and the color.
We see that the color "Primary" has been set as an HSV color, with no mention of the color style. Again with the experimental feature we may be able to get this color style, but for now it is a raw color.

{
  "id": "buttonText",
  "size-constraints": {
    "width-constraints": {
      "sizing-mode": "proportional",
      "value": "1.0"
    },
    "height-constraints": {
      "sizing-mode": "proportional",
      "value": "1.0"
    }
  },
  "font-weight": "700.0",
  "color": {
    "alpha": "1.0",
    "hue": "38.4375",
    "saturation": "1.0",
    "value": "0.25098039215686274"
  },
  "text-content": "Test Text",
  "overflow": "visible",
  "max-lines": "-1",
  "text-align-vertical": "center",
  "line-height": "1.25",
  "typeface": "Quicksand"
}
Enter fullscreen mode Exit fullscreen mode

buttonText gets to keep its name, and here we can see it has sizing constraints, color, text content, alignment and Typeface.

Here we see that the typeface we nicely set in Figma is gone. Instead we have the name of the font, a font weight rather than a style, and a line height. Strangely the size of the font doesn't appear. It's not the worst, but it does come across a little messy.

Ok! We've imported the json config, now we can build it and take a look at our generated compose code!

Step 3. Generated Code

@Composable
fun ButtonPrimary(
    modifier: Modifier = Modifier,
) {
    TopLevel(
        modifier = modifier
    ) {
        ButtonText(modifier = Modifier.rowWeight(1.0f).columnWeight(1.0f))
    }
}
Enter fullscreen mode Exit fullscreen mode

Here we can see what looks like a straight forward compose function, with the atoms defined from the json. But what are these TopLevel and ButtonText functions? Well, as you can imagine there's a too much generated code to go into for this post, so for brevity I'm going to highlight some key points. If you want to dig deeper into what's generated, I've created a github gist here that contains the generated code. Now let's go over some noteworthy code.

Relay Wrappers

All of the code generated from Relay has wrapping compose functions with the prefix Relay. For example the ButtonText function contains the RelayText function, which takes in the properties defined in Figma. These wrapper functions are generated in the relay-runtime package while our particular Button is defined in the relay package.

This goes even as far as having a RelayColumn and RelayRow, which strangely do not wrap their compose counterparts.

Properties

The properties in these wrappers have the same names as mentioned in figma, rather than compose names. For example instead of horizontalArrangement we have mainAxisAlignment. Similarly while Relay tries to use as many generic types as possible (i.e. floats, booleans, etc) it doesn't always use Compose types. It does some types like Color, FontWeight and more, but avoids others like Alignment and TextDecoration. I'm sure there are technical reasons for this but it really does make it feel like a plugin generated it.

Conversion

The conversion is interesting, albeit a bit messy. A lot of conversions are simply when statements, comparing the Figma variables to the compose variables. Most of the conversion is directly in the compose function, rather than having extensions or organizing the code a bit more. Generally speaking for the whole generated code, it's a bit messy and hard to read, but it gets the job done.

Bonus: Comparing to Typical code

Now that we've seen what the generated code looks like and how we've gotten there, I decided it would be fun to compare this to how I would personally create this component. Below is my version:

Button(
  onClick = { /*TODO*/ },
  modifier = Modifier.fillMaxWidth(1.0f),
  shape = RoundedCornerShape(15.dp),
  elevation = ButtonDefaults.elevation(defaultElevation = 0.dp),
  colors = ButtonDefaults.textButtonColors(backgroundColor = DesignColors.primary),
) {
  Text(
    text = text,
    style = typography.button,
    color = DesignColors.onPrimary,
    modifier = Modifier.padding(10.dp),
  )
}
Enter fullscreen mode Exit fullscreen mode

Here I'm using the base jetpack components, with the addition of creating text and color styles for re-use.

It's interesting to see the differences between what a developer might make and what Relay is generating. If you look past the wrappers and name changes both functions are eventually calling the same code. Both use RoundedCornerShape, padding, and elevation, even if you have to dig deeper to see it. The only interesting difference is Relay does not use the composable Button or Text functions, but stick with their own custom implementation. That is to be expected when Figma has no concept of android components like buttons.

So yea, you can see that while different they both use similar code. Of course my code is more concise and easier to read, but does it really matter if no developer is going to manually update the composable?

Conclusion

So that's what's going on under the hood in Relay! There are some things I unfortunately didn't have time to cover such as nested components(spoilers, they work as expected!), adding custom parameters in Figma, mapping styles to compose, and vectors. If these are topics that you'd like me to cover let me know in the comments. Also if you have any questions or would like to see more of the code leave a comment or reach me at my twitter, dev.to, or on the Kotlin Slack (All under @kevinschildhorn).

Top comments (0)