Building a Living Design System
How We Created an Automated Android Component Catalog That Actually Works
By Pedro Santos
Creating effective design systems is notoriously difficult. Everyone’s aware of the benefits, but most stories – for whatever reason – don’t have a happy ending.
At Doist, we wanted our experience to be different.
We started the design side of our design system some time ago and made big strides on that front. When it came the time to map design to code, we struggled. Our codebase and processes weren’t designed to have a design-system first approach, so we had to work through it.
This article focuses specially on the components part of the design system and how we came up with an automated Components Catalog that makes the bridge between the components’ design and the code that implements it.
Let’s dive in.
Doist’s design system
Our design team has spent the last few years building and iterating on our design system.
It’s hosted in Figma and consists of four pillars:
- Colors - A set of color groups (e.g.
actionable
,display
) and attributes within each group (e.g.displayPrimaryIdleTint
) with which we can represent all of Todoist’s themes. - Text styles - A set of text styles (e.g. text size, line spacing, etc) that we should use to, well, style our text.
- Icons - A set of icons that we can use throughout the app, each with some possible variations (e.g. sizes, filled vs outlined, etc).
- Components - A set of base components, that leverage the 3 pillars referenced above, used to build the UIs across all our features. This is where our greatest challenge lies.
Doist didn’t want to have this design system just on the design side. The organization wanted to port this to our apps and map it to code that actually works, to leverage all the possibilities that having a well-integrated design system opens up for us.
The Problem
Something that’s not automated or easy to check automatically tends to rot. This is what we observed with our original design system implementation.
Someone implemented one version of a component in the scope of a given feature, then another person on the team wasn’t aware of it or had a design spec with a similar but slightly different component, ending up implementing a different version. Suddenly, there are four variations of the same button which we would have to deal with sooner or later.
But these struggles are bidirectional.
Our design team also felt the pain of not knowing which UI elements we had built as reusable components versus one-off feature code, which subset of their designs we had actually implemented, and how our implementations looked compared to their specs. The disconnect was also conceptual—designers might consider something one component while we treated it as multiple unrelated pieces of UI that happened to look alike.
This was harming our adoption of the design system, and we wanted to fix it at the root.
Our solution: An Automated Component Catalog
We knew that, whatever solution we came up with, had to be easy to adopt. It had to be as automated and as seamless in our day to day as possible. Here’s the breakdown of how we adapted and structured our codebase and tools to embrace a design-system first mindset.
The Foundation: Smart Module Architecture
We organized our components into focused modules:
-
app-ui
: Where all non design system UI – e.g. screens, one-off components – of the app is in. -
app-ui-system
: Houses all design system components specifically for our main Todoist app. -
ui
: Contains shared elements, such as color definitions, used across both Android and WearOS apps.
This separation keeps things clean and sets a clear boundary of what are one-off pieces of UI, what are effectively components and where to search for each of these.
Component Guidelines That Actually Matter
Instead of vague principles, we established concrete, enforceable rules:
Naming Consistency: Component names must exactly match its definition in Figma. No exceptions. This simple rule eliminates the mental overhead of translating between design and code.
Compose-first: Our codebase has both Views
and Compose
code, but we decided from the get-go that our components would be Compose-only. We could leverage AbstractComposeView
as a bridge if needed.
No Magic Values: Every component uses our theming system and is parameter-driven, no magic values and resources are passed already resolved. This makes them reusable and predictable.
Documentation & Testing Mandatory: Every component requires thorough kdoc
documentation, multiple @Previews
that showcase all possible states for each component and Paparazzi snapshot tests. This isn’t just about testing—these previews become the visual documentation in our catalog.
How we automated it all
After having all the necessary pieces in place to have a great component catalog with the same quality standard as our frontend team’s Storybook , we needed to actually generate and deploy it.
For that, we use 3 main tools:
Dokka
We chose Dokka to generate the component catalog’s info from our component’s kdoc
. This ensures that code and documentation are in sync. As we have markdown support for free, we can format the text in a way that looks good when converted to HTML.
Here’s how the documentation of one of our components looks like:
/**
* A composable that displays a switch component. [Figma](link to the component definition in Figma")
*
* The switch is a toggle component that allows users to enable or disable a setting or feature.
*
* @param checked The current state of the switch (true for on, false for off).
* @param onCheckedChange Callback invoked when the user clicks the switch to change its state.
* @param modifier Optional [Modifier] to be applied to the switch.
* @param enabled Whether the switch is enabled and can be interacted with. When disabled,
* the switch will appear in a non-interactive state.
*
* # Examples:
* <img src="Name of the snapshot generated by Paparazzi that showcases how the component looks like"/>
*
* @includeTokenInfo
*
*/
We first describe the general behavior of the component and its purpose, with a link to its design counterpart.
Then we describe its parameters.
Then we have an Examples section, where we add as many <img>
tags as we want and the source for these are our Paparazzi
snapshots.
Lastly, we include a custom @includeTokenInfo
annotation that is a placeholder for displaying which tokens – colors, text styles – are used in this component. More on this below.
A bit of scripting wizardry
We extracted 2 important steps to scripts.
Inline image previews
The first is a shell script that reads which Paparazzi
snapshots are referenced in the documentation and then copies them to the component catalog directory so they can be embedded as inline previews in the docs.
Here’s the structure of our script.
#!/bin/bash
# Ensure git lfs is available
if ! command -v git-lfs &> /dev/null; then
echo "❌ Error: git-lfs is not installed"
exit 1
fi
# Ensure we have the actual LFS content for the snapshots images
echo "📥 Pulling Git LFS content for snapshots images..."
git lfs pull --include="path/to/your/paparazzi/snapshots/images"
# Find all HTML files and process their image references
echo "🔍 Searching for image references in HTML files..."
find component-catalog -type f -name "*.html" | while read -r html_file; do
# Get the directory of the HTML file
html_dir=$(dirname "$html_file")
# Extract image references from this HTML file
echo "📄 Processing HTML file: $html_file"
grep -h -o 'src="[^"]*\.png"' "$html_file" | \
sed 's|src="||' | \
sed 's|"||' | \
sort -u | \
while read -r img; do
echo "📄 Found reference to image: $img"
if [ -f "path/to/your/paparazzi/snapshots/images/$img" ]; then
echo "📦 Copying image: $img to $html_dir/"
# Copy with preserved permissions and ensure it's readable
cp -p "path/to/your/paparazzi/snapshots/images/$img" "$html_dir/$img" && \
chmod 644 "$html_dir/$img"
echo "✅ Successfully copied: $img"
else
echo "❌ Error: Referenced image not found in snapshots: $img"
exit 1
fi
done
done
Inline colors and text styles references
The second is a Kotlin script that grabs all references to our design system colors and text styles in a given component’s file, and then replaces our custom @includeTokenInfo
annotation with a formatted text listing all those references.
This helps our design system understand which colors and text styles are used by a given component and identify any possible outliers that aren’t clear from the inline previews.
import java.io.File
// Directory containing the components to scan.
val ComponentsSourceDir = File("path/to/your/components/directory")
// Colors format is AppTheme.colors.<group>.<color>
private val RegexColors = "AppTheme\\.colors\\.[a-zA-Z0-9_]+\\.[a-zA-Z0-9_]+".toRegex()
// Typography format is AppTheme.typography.<type>>
private val RegexTypography = "AppTheme\\.typography\\.[a-zA-Z0-9_]+".toRegex()
// The component's KDoc contains a custom @includeTokenInfo annotation that we'll replace and inject
// the generated tokens' info.
private val RegexIncludeTokenInfo = Regex(
pattern = """(@includeTokenInfo)(.*?)?(\*\/)""",
options = setOf(RegexOption.DOT_MATCHES_ALL)
)
/**
* Gathers all colors and typography references in [content] and transforms them into a Markdown
* formatted list.
*/
private fun buildTokenInfo(content: String): String {
val colors = RegexColors.findAll(content)
.map { it.value }
.toSortedSet()
.ifEmpty { setOf("Could not find any color references in this component.") }
val typography = RegexTypography.findAll(content)
.map { it.value }
.toSortedSet()
.ifEmpty { setOf("Could not find any typography references in this component.") }
return """
# Tokens used in this component:
* ## Colors
${colors.joinToString(separator = "\n") { " * - `$it`" }}
* ## Typography
${typography.joinToString(separator = "\n") { " * - `$it`" }}
*
""".trimMargin()
}
println("🔍 Scanning in ${ComponentsSourceDir.absolutePath}")
ComponentsSourceDir.walkTopDown()
.filter { it.extension == "kt" }
.forEach { file ->
println("🔨 Processing file: ${file.name}")
val fileContent = file.readText()
val updatedContent = RegexIncludeTokenInfo.replace(fileContent) { matchResult ->
// Optional content after the annotation.
val restOfKDoc = matchResult.groupValues[2]
// KDoc's closing */ element.
val kDocEnd = matchResult.groupValues[3]
// Replace the annotation with token, preserving whatever was after it.
"${buildTokenInfo(fileContent)}${restOfKDoc}$kDocEnd"
}
if (fileContent != updatedContent) {
println(" ✅ Updated ${file.path}")
file.writeText(updatedContent)
} else {
println(" ❌ ${file.name} did not include any @includeTokenInfo annotation.")
}
}
This script turns this:
*
* @includeTokenInfo
*
Into this:
* # Tokens used in this component:
* ## Colors
* - `AppTheme.colors.selectable.background`
* - `AppTheme.colors.selectable.primarySelectedFill`
* - `AppTheme.colors.selectable.primaryUnselectedTint`
* ## Typography
* - `AppTheme.typography.Body2`
An important note is that we don’t commit the transformed version into the repository. This ensures that, every time the script runs on CI we update it to have all the latest token references.
GitHub Actions & Pages
We leverage GitHub Actions to update the documentation and publish it in a 2-step process:
Step 1 – Documentation Generation
When someone merges a PR that touches our app-ui-system
module, our first workflow springs into action. It updates the documentation (including images and tokens) and creates a new PR with the updated docs.
The author of the changes that triggered that documentation update is requested as the reviewer so they can validate their changes. Here’s how that workflow looks like:
name: Check component catalog updates
on:
pull_request:
types:
- closed
branches:
- main
paths:
- 'app-ui-system/**'
jobs:
component-catalog-update:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
token: ${{ secrets.DOIST_BOT_TOKEN }}
submodules: recursive
fetch-depth: 0
lfs: true
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: true
- name: Inline tokens info
run: kotlinc -script <your_inline_tokens_script>
- name: Generate Component Catalog (dokka) documentation
run: ./gradlew :app-ui-system:dokkaHtml
- name: Discard changes to component source files
run: git checkout -- <path to your components' directory>
- name: Copy preview images
run: <your_copy_images_script>
- name: Check for documentation changes
id: git_diff
run: |
git fetch origin main
git add component-catalog
git diff --quiet origin/main -- component-catalog || echo "changed=true" >> $GITHUB_OUTPUT
- name: Create Pull Request with updated docs
if: steps.git_diff.outputs.changed == 'true'
uses: your-preferred-create-PR-action
with:
# All relevant attributes.
reviewers: ${{ github.event.pull_request.user.login }}
Step 2 – Publishing
When that documentation update PR gets merged, our second workflow deploys everything to GitHub Pages.
name: Publish Component Catalog to GitHub Pages
on:
pull_request_target:
types: [ closed ]
branches:
- main
permissions:
contents: read
pages: write
id-token: write
jobs:
component-catalog-publish:
# Only run if the PR was merged and has the component-catalog branch prefix.
if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'component-catalog/')
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Upload artifact for GitHub Pages
uses: actions/upload-pages-artifact@v3
with:
path: ./component-catalog
- name: Deploy to GitHub Pages
uses: actions/deploy-pages@v4
The result
An online component catalog that’s always up-to-date with the latest code and available for all stakeholders to check on the platform of their choice.

The Developer Experience
One of our requirements for this component catalog and design system implementation was for the developer experience to create components and keep the documentation up-to-date to be as smooth as the design team’s experience interacting with it.
So, before rolling this out to the whole team, we had the following:
- The
ui-system
module setup and some existing components moved there. - Updated those components to comply with the guidelines.
- Added a custom
@Preview
annotation andPaparazzi
extension to make it transparent to showcase and test components in all themes we currently support. - The automation workflows in place.
- Detailed documentation on how all the different pieces fit together to reduce friction.
By having a solid infrastructure with examples and a clear workflow, the adoption of this idea and process went very smoothly.
Why This Approach Works
Minimal Maintenance Overhead: Developers don’t need to remember to update documentation. It happens automatically as part of their normal workflow.
Visual First: Every component shows up in the catalog with actual rendered previews, not just code snippets. Designers can see exactly how their designs translate to code.
Single Source of Truth: The catalog reflects the actual implementation, not some idealized version that exists only in documentation.
Quality Gates: The Paparazzi
snapshot tests catch visual regressions before they make it to production, while the preview requirements ensure every component is actually usable.
Designers are also editors: All this happens within GitHub and is based on human-written and readable text, so our designers can open pull requests and propose updates to the documentation by themselves.
Lessons Learned
Automation is Everything: The moment documentation becomes a manual step, it starts falling behind. Automate relentlessly.
Constraints Enable Creativity: Our seemingly strict guidelines actually make developers more productive by eliminating decision fatigue and ensuring consistency.
Make the Right Thing Easy: When following best practices is the path of least resistance, teams naturally build better software.
Alternatives Considered
Before settling on our automated catalog approach, we explored several other options:
- Showkase : We gave this library a serious try, but quickly discovered that browsing components within an Android app wasn’t practical for our primary stakeholders, the design team, since they work primarily on desktop machines rather than Android devices, they couldn’t easily access or review the component catalog.
- Example app: Building a dedicated showcase app seemed straightforward initially, but the reality of code duplication and ongoing maintenance quickly became apparent. Plus, we’d still face the same fundamental issue: our designers wouldn’t be able to easily access it without Android devices.
- Compose Multiplatform: While this showed promise as an interesting R&D project direction that could eventually make it easy to display the compose components in a web page, the implementation complexity exceeded our intended timeline and available resources. Namely, decoupling components from the material library, and having to implement and maintain a multiplatform project within a team that’s not yet very skilled on it.
- Storybook : The familiar tooling was appealing, but it had 2 major problems: 1) Having to build and keep a showcase app up to date and 2) it’d introduce significant costs that were difficult to justify for a single-platform solution.
- Storytale : Designed for KMP projects, which isn’t our case.
Building a component catalog that teams actually use requires more than just good documentation — it requires rethinking how design systems integrate into your development workflow. By automating the tedious parts and focusing on developer (and designer) experience, you can create something that truly serves your team instead of becoming another maintenance burden.
Given this is a yet-to-be-figured-out challenge in the Android community, we hope that by sharing how we approached the problem we can help other teams and spark more discussions on how to make this a useful tool rather than yet-another tricky problem to solve.
Questions, suggestions? Anytime. 🚀