Skip to content
fitme·story
Case studies
v7.10 · 4 min read · 2026-06-17

Android Token Pipeline — making the design system real (AND-1)

The android-design-system feature shipped 2026-04-04 as research-only — an iOS→Material-Design-3 mapping doc, an adaptation strategy, and a Style Dictionary config that generated nothing. Two and a half months later the config was broken (referenced non-existent transforms, written against SD v3 while the repo had migrated to v5) and the mapping doc was stale (92 tokens against a source restructured to 108). This ships the real token layer: mirror the working iOS pipeline's hand-rolled-format philosophy for Android, generating committed, drift-gated Kotlin + XML artifacts. 49 colors + 34 dimens + 8 Compose objects, byte-identical across builds, with a drift gate that fails on any token edit. Honest limit — the Kotlin is structurally valid and drift-gated but NOT compiled (no Android toolchain in-repo).

49 + 34 + 8
colors + dimens + Compose objects — committed, byte-identical across builds, behind a drift gate that fails on any token edit
The 2026-04-04 android-design-system shipped research-only: a Style Dictionary config that generated nothing, broken against the v3→v5 migration. This ships the real token layer — committed Kotlin + XML artifacts that are byte-identical across two consecutive builds. Honest limit: the Kotlin is structurally valid and drift-gated but NOT compiled (no Android toolchain in-repo).
What do T1 / T2 / T3 mean?
T1Instrumented — from a ledger/commit
T2Declared — stated, not measured
T3Narrative — estimate from memory

Android Token Pipeline — making the design system real (AND-1)

The problem: a design system that was "Android" in name only

The android-design-system feature shipped 2026-04-04 as a research-only deliverable — an iOS→Material-Design-3 mapping doc, an adaptation strategy, and a Style Dictionary config. But it generated nothing. Two and a half months later the reality (T1, audited 2026-06-17):

  • design-tokens/config-android.json was broken — it referenced Style Dictionary transforms size/compose/dp and size/compose/sp that don't exist (the real compose group ships rem-input transforms), and it was written against Style Dictionary v3 while the repo had since migrated to v5 (2026-06-08). npm run tokens had no Android target. No .kt or .xml existed anywhere.
  • docs/design-system/android-token-mapping.md was stale — documented 92 tokens against a source that had restructured to 108 (66 actionable).

This is precisely the silent-gap class the FitMe framework exists to kill: a config that looks like a pipeline but produces no artifact, with a doc that looks current but tracks a two-month-old schema. No gate caught it because nothing consumed it — the classic "no consumer, no signal" blind spot.

What shipped

Mirror the working iOS pipeline (sd.config.mjsDesignTokens.swift, gated by make tokens-check) for Android, reusing its hand-rolled-format philosophy so the output depends on no built-in Style Dictionary transform group (the thing that broke the original config across the v3→v5 migration).

  • sd.config.android.mjs (SD v5, ESM) — custom Compose + XML formats. Colors: hex and rgba(r,g,b,a)Color(0xAARRGGBB) / #AARRGGBB (e.g. rgba(0,0,0,0.84) → alpha 0xD6). Spacing/radius/size/layout/shadow px → Dp / <dimen>Ndp. Opacity → Float. Motion {easing,duration} → duration-ms + easing comment. The 16 typography.* tokens are semantic style strings (largeTitle/bold/rounded), not numeric sizes — so there's nothing to emit as sp; they're a reference comment block pointing at the MD3 type-scale mapping.
  • Generated + committed artifacts: android/FitMeDesignTokens.kt (FitMeColors / FitMeSpacing / FitMeRadius / FitMeSize / FitMeLayout / FitMeElevation / FitMeOpacity / FitMeMotion), android/res/values/colors.xml (49), android/res/values/dimens.xml (34).
  • Drift gate: npm run tokens:android + tokens:android:check (scripts/check-tokens-android.js — regenerate, strip date lines, diff, restore; execFileSync with fixed argv, no shell).
  • Removed the broken design-tokens/config-android.json (superseded).
  • Refreshed android-token-mapping.md (§0a "AND-1 SHIPPED") + android-adaptation.md (generated token layer; fixed a stale Focus.ring row removed in audit DS-015).

Decisions

  • Compose + XML, not XML-only (operator choice) — the complete, modern-Android-consumable output. Marginal cost over XML-only was ~1h.
  • Raw-pixel Dp, not rem-scaled — cross-platform parity with iOS CGFloat direct values; the original rem-input assumption was the root of the broken transforms.
  • Hand-rolled formats — like the iOS config; immune to Style-Dictionary built-in-format drift, which is exactly what broke v1.

Verification (T1)

  • npm run tokens:android → 3 artifacts, 0 errors.
  • Two consecutive builds → byte-identical (diff clean).
  • tokens:android:check → exits 0 in-sync; non-zero after a token edit (verified mutate-then-revert).
  • 49 colors + 34 dimens, 0 duplicate resource names; Kotlin val-name "collisions" (small, medium) are across different objects — valid Kotlin.

What this is NOT

This builds the token layer, not an app. No Gradle, no Compose screens, no AndroidManifest.xml. android-app-implementation (AND-3) remains deferred. The value here: the Android design system is now a real, generated, drift-gated artifact instead of a broken config and a stale doc — ready to consume the day an app is warranted.

Follow-ups

  • make tokens-android-check + verify-local wiring (infra-glob → isolated worktree, separate PR).
  • This showcase MDX + related_prs backfill (post-merge close-feature).

Cross-references

Honest disclosures
  • The generated Kotlin is NOT compiled — there is no Android toolchain in-repo — so verification is determinism + structural validity + the drift gate, not gradlew assemble; the kill criterion "Kotlin won't compile in a real Android module" therefore cannot yet be evaluated.
  • This ships the token layer only, not an app: no Gradle, no Compose screens, no AndroidManifest.xml. The native Android app (AND-3) remains deferred indefinitely (re-eval 2027-05-26).
  • The pipeline emits no typography sizes — the 16 typography.* tokens are semantic style strings (e.g. largeTitle/bold/rounded), not numeric sp values, so they ship as a reference comment block pointing at the MD3 type-scale mapping rather than generated dimens.
Kill criterion · not fired
  • Generated Kotlin won't compile in a real Android module
  • Drift gate false-positives on a clean tree
  • Style Dictionary v6 removes the v5 format API