Mapping components to existing code

Developers can customize the code generation process by providing a mapping between a UI Package and an existing code component instead of the generated code. This is beneficial when the existing implementation has features that cannot be achieved by the generated code such as animation or complex behavior (such as a drop down menu).

Developers specify how to map components using a mapping file. A mapping file tells the code generator, at minimum, how to reach the desired composable function so that the right client code can be created.

Mapped component overview diagram

Here is an example:

In Figma, a designer creates a Card component that contains an instance of a Play Bar component, packages both components, and sends them to a developer.

When the developer imports the UI packages from Figma, two directories are created in ui-packages: card and play_bar. When they build the project, two composable functions are created: Card and PlayBar. Typically, because Card contains a Play Bar instance in Figma, in code the Card composable function contains a call to the PlayBar composable.

However, the designer and developer want Card to instead use an existing composable, MyExistingPlaybar, which has functionality that is hard to describe in Figma. So the developer adds a mapping file called play_bar.map that maps the play_bar UI package to MyExistingPlaybar:

{
    "target": "MyExistingPlaybar",
    "package": "com.example.myApp"
}

Now, when the developer builds the project, Card calls MyExistingPlaybar instead of PlayBar. Note that MyExistingPlaybar must have the same parameters as PlayBar (although there can be a few differences, as described in Additional Directives below).

Mapping file

In your Android Studio projects, mapping files are added under ui-package-resources/mappings next to the ui-packages folder. Relay will look for your mapping files during build.

Mapping file in the project view

Mapping file name

The name of a given mapping file must match the name of the UI Package folder for the component it replaces. So play_bar.map maps the UI Package in the ui-packages/play_bar folder to an existing code component.

Mapping file contents

The mapping file contains the following properties:

  • target : (Required) The name of your custom composable function. By default, this is the name of the function created by generated code.

    "target" : "CustomComposableName"
    
  • package : (Required) Name of the package that your custom composable sits in. By default, this is the package of the function created by generated code.

    "package" : "com.example.podcastapp.ui.components"
    
  • generateImplementation : (Optional) true or false. If true, an implementation of this UI package is still created in the generated code file. If false, the implementation isn't created. By default, this is true.

    "generateImplementation" : true
    
  • generatePreviews : (Optional) true or false. If true, a preview of the mapped custom component is created in the generated code file. If false, no preview is created. By default, this is true.

    "generatePreviews" : true
    

Mapped variants

If a Figma component has variants, then the generated composable will have enum parameters that encode the variant (as described in the Handling Design Variants tutorial). If you want to map a Figma component with variants to existing code, it needs to be mapped to a composable that takes the same parameters as the generated composable, except that the enum parameter is replaced by a string parameter called design. For example, suppose there is a Figma component called Chip with a variant whose property is ChipType. Chip’s generated composable signature looks like this:

@Composable
fun Chip(
    modifier: Modifier = Modifier,
    chipType: ChipType = ChipType.Red,
    chipText: String
) { ... }

If you want to have the Chip Figma component map to an existing MyChip composable, then the signature for MyChip must have the same signature as the generated composable (assuming no additional directives are specified). Conceptually, this suggests that the existing code component is capable of the same design variants as the Figma component.

Additional directives

Suppose the composable function we want to target has the following signature:

@Composable
fun MyChip(
    modifier: Modifier = Modifier,
    chipType: ChipType = ChipType.Red,
    description: String  // instead of chipText
) { ... }

We can add a fieldMappings block to the mapping file that affects how parameters are mapped. In this case, it contains a mapping from the chipText parameter in the Chip to the description parameter in MyChip.

{
    "target": "MyChip",
    "package": "com.example.myApp",
    "fieldMappings": [
        {
            "type": "parameter",
            "source": "chipText",
            "target": "description"
        }
    ]
}

The types for the fieldMappings block include:

  • type: parameter maps a UI package field to a code parameter.
    • source: Name of parameter as specified in the UI package.
    • target: Name of parameter as specified in the target code component.
  • type: lambda maps a UI package field to a content lambda.
    • source: Name of parameter as specified in the UI package.
    • target: Name of parameter as specified in the target code component.
  • type: modifier maps a UI package field to a modifier method. Note: This mapping type is being incubated, in particular the handling of receiver scopes.
    • source: Name of parameter as specified in the UI package.
    • method: Method on the Modifier object that should be invoked in generated code.
    • parameter: Name of parameter within the specified Modifier method.
    • library: The qualified package name to import to access the Modifier method.
    • scope: One of two values to indicate the scope of the Modifier:
      • any: The modifier can be used in any receiver scope.
      • relay: The modifier must be used in the receiver scope of Relay’s RelayContainer object.