Introduction
In the world of digital product development, maintaining a cohesive and consistent user interface across different platforms is essential for delivering a seamless user experience. A design system is a comprehensive set of guidelines and standardized components ensuring consistency. It includes foundational elements such as colors, typography, icons, and spacing, which serve as the building blocks for creating a uniform look and feel.
Design systems are crucial for aligning design and development teams, but manually updating and maintaining these systems can be cumbersome and error-prone. This is where automation comes into play. By automating the integration of design systems into your development workflow, you can streamline processes, reduce errors, and ensure that design changes are reflected consistently across your application.
While automation can be applied not only to foundational elements but also to high-level components such as buttons, checkboxes, and other UI elements, this article will focus on automating the foundational elements to keep the discussion concise and more accessible. In this article, we’ll explore how to automate these elements specifically for Android development using Jetpack Compose. We will cover the benefits of this automation, walk through the process of setting it up, and provide practical examples to help you implement it in your own projects.
Benefits of automation
Automating a design system offers numerous advantages, crucial for maintaining high-quality and efficient development processes. Here are some of the key benefits:
- Enhanced consistency across platforms: Automation ensures that design tokens and components are uniformly applied across all products and platforms. This consistency helps maintain a cohesive brand identity and user experience, eliminating discrepancies that often arise from manual implementation.
- Reduced risk of human error: By automating the process, you minimize the risk of errors that can occur during manual handoffs. Design tokens are processed programmatically, reducing the likelihood of mistakes that can lead to inconsistencies or bugs in the UI.
- Faster updates, iterations, and reduced development time: Automation allows design changes made by the design team to be quickly propagated to the development environment, eliminating manual updates. Designers can update the design system in their tools without needing to wait for developers, speeding up iteration cycles. This streamlining of updates also reduces development time by freeing developers from repetitive tasks, allowing them to focus on building features and resolving more critical issues.
- Scalability: As your application grows and evolves, maintaining consistency across an expanding set of components and design elements can be challenging. Automation scales effortlessly with your design system, allowing for easy management and integration of new components or updates.
For example, imagine a scenario where a design team decides to update the brand’s primary color. With an automated design system, this change is made once in the design tool, and the updated color token is automatically propagated to all applications and platforms—whether web, mobile, or desktop—without any manual intervention. This automation prevents potential errors that could arise from manual updates, such as incorrect color codes or missing changes.
Contract between design system and applications
To ensure that design elements are consistently applied across different applications, it is crucial to establish a reliable and standardized connection between the design system and the product codebase. In our approach, this connection is facilitated through a JSON file that acts as a contract between the design system and the applications.
Here’s a step-by-step overview of how this contract works:
- Design and token definition: Designers use the ‘Tokens Studio for Figma’ plugin to define foundational design elements such as colors, typography, and spacing directly within Figma. These design tokens represent the core attributes that will be used across all applications.
- Exporting tokens: Once the design tokens are defined, they are exported from Figma into a JSON file. This JSON file, often referred to as the design tokens file, contains all the necessary details about design elements.
- Synchronization with GitLab: The JSON file is then uploaded to a central repository, such as a GitLab repository dedicated to the design system. This repository serves as the single source of truth for design tokens and ensures that any changes made by designers are centrally managed.
- Automated integration: When the JSON file is updated, automated jobs are triggered through GitLab CI pipelines. These jobs handle the downloading of the updated JSON file, converting it into code that is compatible with the target platform—in this case, Kotlin for Jetpack Compose.
- Code generation and application: The converted code is then integrated into the application’s codebase. This automated integration ensures that the latest design tokens are seamlessly incorporated into the app, providing a consistent look and feel that aligns with the design system.
- Implementation in apps: With the automated code generation in place, developers can use the updated design tokens directly in their code. This approach ensures that all UI components adhere to the design system’s specifications, maintaining visual consistency and enhancing the user experience.
By leveraging this automated contract, design and development teams can work more efficiently and ensure that design specifications are accurately reflected in the final product.
Tokens defined by designers
This section contains examples of how we at DISQO define and utilize the design tokens.
Color tokens
Color tokens are used to maintain a consistent color palette throughout the application. Each token represents a specific color used in different UI elements. For example:
backgroundPrimary
: Defines the primary background color used across the app.
contentPrimary
: Specifies the primary color for text and key content elements.
Typography tokens
Typography tokens manage text styles to ensure readability and visual harmony across various devices and screen sizes. Each token defines a specific style for text elements, including font size, weight, and line height. Examples include:
labelExtraSmallDefault
: Sets the style for extra small labels.
paragraphDefault
: Defines the default styling for paragraph text.
Here is how these tokens are used in the Android app:
Text( text = cardInfo.name, **style = AppTheme.typography.labelSmallDefault,** **color = AppTheme.applicationColors.content.primary,** )
|
Android code structure
Our approach involves structuring code so that screen UIs are composed of reusable components, which in turn utilize foundational design tokens. For instance, a button component on a screen might use AppTheme.applicationColors.content.onColor
for its text color and AppTheme.typography.title01
for its text style. These references are linked to auto-generated classes based on design tokens. For example, our content color tokens are defined in ApplicationContentColorTokensGenerated
:
internal object ApplicationContentColorTokensGenerated {
val primary = Color(0xFF2C2C2C) val secondary = Color(0xFF6E6E6E) val onColor = Color(0xFFFFFFFF) ... }
|
Typography tokens are similarly defined in TypographyTokensGenerated
:
internal object TypographyTokensGenerated {
val titlesNormal01 = TextStyle( fontFamily = FontFamilies.poppins, fontWeight = FontWeight.SemiBold, fontSize = 18.sp, lineHeight = 26.sp, ) ... }
|
Tokens JSON file structure
These classes, ApplicationContentColorTokensGenerated
and TypographyTokensGenerated
, are directly generated from the figma-tokens.json
file, ensuring that any design updates in Figma can quickly propagate to the codebase. Here’s how the JSON structure for these tokens appears:
{ "global": { "Application": { "Content": { "contentPrimary": { "value": "#2c2c2c", "type": "color" }, "contentSecondary": { "value": "#6e6e6e", "type": "color", "description": "Use for Secondary Content" }, "contentOnColor": { "value": "#ffffff", "type": "color" }, ... }, ... }, ... } }
|
{ "global": { "typography": { "Titles - Normal": { "01": { "value": { "fontFamily": "{fontFamilies.poppins}", "fontWeight": "{fontWeights.poppins-3}", "lineHeight": "{lineHeights.3}", "fontSize": "{fontSize.3}", "letterSpacing": "{letterSpacing.1}", "paragraphSpacing": "{paragraphSpacing.0}", "textCase": "{textCase.none}", "textDecoration": "{textDecoration.none}" }, "type": "typography" }, ... }, ... }, "textCase": { "none": { "value": "none", "type": "textCase" } }, ... } }
|
Python script
Once the figma-tokens.json
file is updated in the GitLab repository, jobs are triggered for each product repository. For the Android repository, the job runs a Python script that performs the following steps:
- Logs into GitLab using the GitLab library and the provided
token
, then downloads the figma-tokens.json
file.
- Converts the JSON file to Kotlin code.
- Commits the changes and opens a merge request (MR) in the Android project.
Let’s delve into the second step. Here is an example of how we create the ApplicationColorTokensGenerated.kt
Kotlin file, parse the background color tokens, and write them as a Jetpack Compose Color
class that can be used in the Kotlin code.
Given JSON structure
{ "global": { "Application": { "Background": { "backgroundPrimary": { "value": "#ffffff", "type": "color" }, "backgroundSecondary": { "value": "#fafafa", "type": "color" }, ... } } } }
|
Python code
Creating the Kotlin file
The following snippet opens a Kotlin file named ApplicationColorTokensGenerated.kt
(or creates the file if it doesn’t exist) and prepares it to write the necessary Kotlin code:
with open(generated_files_path + '/ApplicationColorTokensGenerated.kt', 'w') as application_colors_file: application_colors_json = global_json['Application'] application_colors_file.write('package com.example.ds.generated\n\n') application_colors_file.write('import androidx.compose.ui.graphics.Color\n\n')
application_colors_file.write('internal object ApplicationBackgroundColorTokensGenerated {\n\n') parse_colors_and_write_in_file( file = application_colors_file, colors_json = application_colors_json['Background'], prefix_to_remove = 'background', ) application_colors_file.write('}\n') ...
|
- File creation: Opens the file in write mode (
'w'
). If the file doesn’t exist, it will be created.
- JSON extraction: Retrieves the colors JSON from the
global_json
dictionary.
- Kotlin code setup: Writes the package name and imports needed for the Kotlin file.
- Object declaration: Declares an internal Kotlin object to hold the background color tokens.
- Color parsing: Calls the
parse_colors_and_write_in_file
function to parse the colors and write them into the file.
Parsing colors and writing to file
The parse_colors_and_write_in_file
function processes each color token and writes it to the Kotlin file:
def parse_colors_and_write_in_file(file, colors_json, prefix_to_remove): for color_name in colors_json: color = colors_json[color_name] if color['type'] == 'color': color_name_val = normalize_variable_name( variable_name = color_name, character_to_remove = '-', prefix_to_remove = prefix_to_remove, ) color_value = ''.join(letter for letter in color['value'] if letter != '#').upper() if len(color_value) == 6: compose_color_value = '0xFF' + color_value elif len(color_value) == 8: compose_color_value = '0x' + color_value[6:8] + color_value[0:6] else: compose_color_value = None if compose_color_value is not None: file.write(' val ' + color_name_val + ' = Color(' + compose_color_value + ')\n')
|
- Iterate through colors: Loops through each color token in the JSON.
- Type check: Ensures that the token type is
color
.
- Normalize color name: Calls
normalize_variable_name
to convert the JSON key to a valid Kotlin variable name.
- Extract and normalize color value: Removes the hash sign (
#
) from the color value and converts it to uppercase. It then formats the color value for Kotlin:
- If the value is six characters long, it prepends
0xFF
(fully opaque).
- If the value is eight characters long, it rearranges the characters to match the ARGB format (
0xAARRGGBB
).
- Write to file: Writes the color definition to the Kotlin file if the color value is valid.
Key Considerations
- Automatic token addition: When designers add a new token, it is automatically included in the Kotlin code. However, developers are responsible for implementing the usage of the new token in the application.
- Token removal handling: If a token used in the code is removed by designers, the merge request (MR) pipeline will fail. Developers must then replace the usage of the removed token with an alternative.
- Token value changes: When designers change the value of a token, no action is required from the developers. The updated value will be automatically reflected in the application.
- Token name updates: Designers may also update token names. To manage this, we use additional objects like
ApplicationColorTokens
. For instance:
private val applicationLightColorTokens = ApplicationColorTokens( background = Background( primary = ApplicationBackgroundColorTokensGenerated.primary, ....
|
Instead of using the generated values directly in the code, they are assigned to other variables. This approach allows us to update the token names by modifying a single file, keeping the rest of the codebase unchanged. We can then schedule naming updates for later, ensuring minimal disruption.
Conclusion
Design system automation significantly streamlines the process of maintaining visual consistency and coherence across different platforms. By leveraging tools like Figma Tokens Studio and automated scripts, design changes can be seamlessly propagated to codebases, minimizing manual errors and accelerating development.
The use of a JSON file as a contract between design and development ensures that all parties adhere to unified standards. While this automation offers numerous benefits, key considerations include managing the addition, removal, and renaming of tokens to ensure smooth integration into applications.
Overall, this approach exemplifies how automation can empower design teams and enhance collaboration between designers and developers, ultimately leading to more efficient development processes and higher quality products.