Android Token Structure: openedx-app-android
Source: core/src/main/java/org/openedx/core/ui/theme/, core/src/openedx/org/openedx/core/ui/theme/Confidence: CONFIRMED — verified directly against codebase
Summary
The Android app uses a Theme module in core/ as the single source of truth for all design tokens, surfaced via Jetpack Compose's Composition Local pattern. Unlike iOS, Android has a two-layer color architecture: raw hex values live in a primitive Colors.kt file (in a flavor-specific source set), and semantic tokens in AppColors reference those primitives. Typography tokens are more complete than iOS — they include lineHeight and letterSpacing. Shapes are fully defined with explicit dp values. There is no spacing token system; all padding is hardcoded at the call site.
Token Architecture Overview
core/
├── src/main/java/org/openedx/core/ui/theme/
│ ├── Theme.kt # OpenEdXTheme composable, Light/Dark palette assembly, CompositionLocal providers
│ ├── AppColors.kt # Semantic color data class (wraps Material3 ColorScheme + custom tokens)
│ ├── AppTypography.kt # Typography data class + LocalTypography CompositionLocal with defaults
│ └── AppShapes.kt # Shape data class + LocalShapes CompositionLocal with defaults
│
└── src/openedx/org/openedx/core/ui/theme/ ← white-label source set (replaceable per brand)
├── Colors.kt # Primitive color values (raw hex, light + dark variants)
└── LocalShapes.kt # Shape instantiation with dp valuesAccess pattern in Compose UI code:
MaterialTheme.appColors.textPrimary // custom semantic color
MaterialTheme.appColors.primary // delegates to Material3 ColorScheme
MaterialTheme.appTypography.bodyLarge // TextStyle with size, weight, lineHeight, letterSpacing
MaterialTheme.appShapes.buttonShape // CornerBasedShapeTheme injection (Composition Local):
// In OpenEdXTheme composable:
CompositionLocalProvider(
LocalAppColors provides colors, // AppColors instance (light or dark)
...
)
// Each composable reads: LocalAppColors.currentToken Levels: What Exists vs. What's Missing
| Token Level | Colors | Typography | Spacing | Shapes |
|---|---|---|---|---|
| Primitive (raw values) | ✓ Present (Colors.kt) | ✗ Not present | ✗ Not present | ✗ Not present |
| Semantic (named by role/intent) | ✓ Present (AppColors) | ✓ Present | ✗ Not present | ✓ Present |
| Component (scoped to a component) | Partial (token names only) | ✗ Not present | ✗ Not present | Partial |
Key difference from iOS: Android has a genuine primitive color layer. Raw hex values (light_primary = Color(0xFF3C68FF)) are defined once in Colors.kt and referenced by semantic tokens — so changing a brand color is a single-point edit. iOS raw values are buried in binary .colorset files with no shared references between them.
Colors
How They're Defined: Two Layers
Layer 1 — Primitive (Colors.kt, flavor source set): Raw Color values defined as top-level Kotlin vals. Two sets: light_* and dark_* prefixed. Some semantic tokens at this layer already reference others (e.g., light_text_accent = light_primary), creating a partial alias chain.
val light_primary = Color(0xFF3C68FF)
val light_text_accent = light_primary // aliases primitive
val light_primary_button_background = light_primary // aliases primitiveLayer 2 — Semantic (AppColors.kt, main source set): A data class whose constructor parameters are all named by intent. LightColorPalette and DarkColorPalette in Theme.kt instantiate this class, mapping primitives to semantic slots. Also wraps a Material3 ColorScheme to make M3 components work natively.
data class AppColors(
val material3: ColorScheme, // Material3 integration
val textPrimary: Color,
val primaryButtonBackground: Color,
...
)White-label mechanism: Colors.kt lives in src/openedx/ — a Gradle source set variant. A brand can replace this entire file with different hex values at build time without touching any other code.
Primitive Color Values (Light / Dark)
Brand / Material3 Core
| Primitive | Light | Dark |
|---|---|---|
primary | #3C68FF | #3F68F8 |
primary_variant | #9ADEFAFF (60% opacity) | #3700B3 |
secondary | #94D3DD | #03DAC6 |
secondary_variant | #94D3DD | #373E4F |
background | #FFFFFF | #19212F |
surface | #F7F7F8 | #273346 |
error | #E8174F | #FF3D71 |
Text Primitives
| Primitive | Light | Dark |
|---|---|---|
text_primary | #212121 | #FFFFFF |
text_primary_variant | #3D4964 | #FFFFFF |
text_secondary | #B3B3B3 | #B3B3B3 |
text_dark | #19212F | #FFFFFF |
text_accent | = primary | #879FF5 |
text_field_border | #97A5BB | #4E5A70 |
text_field_hint | #97A5BB | #79889F |
Semantic Color Inventory (AppColors)
Text
| Token | Notes |
|---|---|
textPrimary | Primary body text |
textPrimaryVariant | Slightly muted primary text |
textPrimaryLight | Light variant (= textPrimary in both modes) |
textSecondary | Muted/secondary text |
textDark | Dark text (inverts in dark mode → white) |
textAccent | Accent-colored text (= primary) |
textWarning | Warning-state text |
textHyperLink | Link text (= primary) |
Text Fields
| Token | Notes |
|---|---|
textFieldBackground | Focused field background |
textFieldBackgroundVariant | Variant (white in light, same surface in dark) |
textFieldBorder | Field border |
textFieldText | Input text color |
textFieldHint | Placeholder/hint text |
Primary Buttons
| Token | Notes |
|---|---|
primaryButtonBackground | Filled button BG |
primaryButtonText | Filled button label |
primaryButtonBorder | Button border (outlined variant) |
primaryButtonBorderedText | Outlined button label |
Secondary Buttons
| Token | Notes |
|---|---|
secondaryButtonBackground | = primaryButtonBackground by default |
secondaryButtonText | = primaryButtonText by default |
secondaryButtonBorder | = primaryButtonBorder by default |
secondaryButtonBorderedBackground | Outlined secondary BG |
secondaryButtonBorderedText | Outlined secondary label |
inactiveButtonBackground | Disabled button BG |
inactiveButtonText | Disabled button text (= primaryButtonText) |
Note: A comment in
AppColors.ktexplicitly states: "The default secondary button styling is identical to the primary button styling. However, you can customize it if your brand utilizes two accent colors."
Surfaces / Cards
| Token | Notes |
|---|---|
cardViewBackground | Card surface |
cardViewBorder | Card border |
divider | Horizontal rule / divider |
bottomSheetToggle | Bottom sheet drag handle |
courseHomeHeaderShade | Header overlay shade |
courseHomeBackBtnBackground | Back button BG in course header |
Semantic Status
| Token | Notes |
|---|---|
warning | Warning state (#FFC94D yellow) |
onWarning | On-warning text (white) |
info | Info state (#3A9AE9 blue) |
infoVariant | Info variant (= primary in light) |
onInfo | On-info text (white) |
successGreen | Success green (#198571) |
successBackground | Success background |
rateStars | Star rating yellow |
certificateForeground | Certificate illustration |
Progress / Course
| Token | Notes |
|---|---|
progressBarColor | = successGreen |
progressBarBackgroundColor | Progress bar track |
gradeProgressBarBorder | Grade bar border |
gradeProgressBarBackground | Grade bar track |
componentHorizontalProgressCompletedAndSelected | #30A171 green |
componentHorizontalProgressCompleted | #BBE6D7 light green |
componentHorizontalProgressSelected | #F0CB00 yellow |
componentHorizontalProgressDefault | #D6D3D1 gray |
assignmentCardBorder | Assignment item border |
Course Dates Timeline
| Token | Notes |
|---|---|
datesSectionBarPastDue | = warning (yellow) |
datesSectionBarToday | = info (blue) |
datesSectionBarThisWeek | #3D4964 / #8E9BAE |
datesSectionBarNextWeek | #97A5BB / #4E5A70 |
datesSectionBarUpcoming | #CCD4E0 / #273346 |
Tab Bar
| Token | Notes |
|---|---|
tabUnselectedBtnBackground | Unselected tab BG |
tabUnselectedBtnContent | Unselected tab icon/text |
tabSelectedBtnContent | Selected tab icon/text |
Auth
| Token | Notes |
|---|---|
authSSOSuccessBackground | SSO success banner |
authGoogleButtonBackground | Google button |
authFacebookButtonBackground | Facebook button (#0866FF) |
authMicrosoftButtonBackground | Microsoft button (near-black) |
Settings
| Token | Notes |
|---|---|
settingsTitleContent | Settings title text (white both modes) |
Material3 Delegates (via AppColors)
AppColors exposes the full M3 ColorScheme surface as computed properties: primary, onPrimary, primaryContainer, secondary, background, surface, error, outline, scrim, inverseSurface, etc. These are used by Material3 components automatically.
Two deprecated aliases for backwards compatibility:
@Deprecated val primaryVariant // → primaryContainer
@Deprecated val secondaryVariant // → secondaryContainerTypography
How It's Defined
AppTypography is a data class with all tokens as TextStyle properties. A fontFamily top-level val assembles all weights from bundled R.font resources. Default values are provided via LocalTypography (a staticCompositionLocalOf).
Font weight map (R.font resources):
| Weight | Resource |
|---|---|
FontWeight.Thin | R.font.thin |
FontWeight.Light | R.font.extra_light, R.font.light |
FontWeight.Normal | R.font.regular |
FontWeight.Medium | R.font.medium |
FontWeight.Bold | R.font.bold, R.font.semi_bold |
Type Scale
Android typography is more complete than iOS — every token specifies fontSize, lineHeight, letterSpacing, and fontWeight. The scale mirrors Material3's type system.
| Token | Size | Line Height | Weight | Letter Spacing |
|---|---|---|---|---|
displayLarge | 57sp | 64sp | Normal | -0.25sp |
displayMedium | 45sp | 52sp | Normal | 0 |
displaySmall | 36sp | 44sp | Bold | 0 |
headlineLarge | 32sp | 40sp | Normal | 0 |
headlineBold ⚠️ | 34sp | 24sp | Bold | 0 |
headlineMedium | 28sp | 36sp | Normal | 0 |
headlineSmall | 24sp | 32sp | SemiBold | 0 |
titleLarge | 22sp | 28sp | Bold | 0 |
titleMedium | 16sp | 24sp | SemiBold | 0.1sp |
titleSmall | 14sp | 20sp | Medium | 0.1sp |
bodyLarge | 16sp | 24sp | Normal | 0.5sp |
bodyMedium | 14sp | 20sp | Normal | 0.25sp |
bodySmall | 12sp | 16sp | Normal | 0.4sp |
labelLarge | 14sp | 20sp | Medium | 0.1sp |
labelMedium | 12sp | 16sp | Normal | 0.5sp |
labelSmall | 10sp | 16sp | Normal | 0 |
⚠️
headlineBold(34sp/Bold) is a non-standard addition not present in the iOS scale. It sits betweenheadlineLarge(32sp) anddisplaySmall(36sp) and breaks the otherwise clean M3 vocabulary.
Shapes
How They're Defined
AppShapes is a data class wrapping both a Material3 Shapes object and custom CornerBasedShape tokens. Values are instantiated in LocalShapes.kt (flavor source set) — making shape values replaceable per brand, same mechanism as colors.
Shape Inventory
Material3 shape scale (used by M3 components automatically):
| M3 Token | Value |
|---|---|
extraSmall | 4dp |
small | 8dp |
medium | 12dp |
large | 16dp |
extraLarge | 24dp |
Custom shape tokens:
| Token | Value | Notes |
|---|---|---|
buttonShape | 8dp all corners | Primary/secondary buttons |
navigationButtonShape | 8dp all corners | Nav bar buttons |
textFieldShape | 8dp all corners | Input fields |
cardShape | 12dp all corners | Content cards |
sectionCardShape | 6dp all corners | Section-level cards |
courseImageShape | 8dp all corners | Course thumbnail |
videoPreviewShape | 8dp all corners | Video preview |
screenBackgroundShape | 30dp top-start + top-end only | Sheet-style screen overlay |
screenBackgroundShapeFull | 24dp all corners | Full-screen rounded background |
dialogShape | 24dp all corners | Modal dialogs |
Spacing
There is no spacing token system. No AppSpacing class, no dimens.xml, no defined spacing scale. All padding and layout values are hardcoded as literals at each call site. Confirmed by absence of any spacing-related file in the theme directory and no dimens.xml in the project.
Same gap as iOS.
Key Design Observations
What the Android token system does well vs. iOS
- Has a real primitive layer —
Colors.ktin a flavor source set gives brand swaps a single point of edit. iOS buries raw values in binary.colorsetfiles. - Composition Local pattern — standard Jetpack Compose approach; tokens flow via the composition tree, not global statics. No concurrency hacks needed (unlike iOS's
nonisolated(unsafe)). - Material3 integration —
AppColorswrapsColorSchemedirectly, so M3 components get themed automatically without manual wiring. - Typography is more complete —
lineHeightandletterSpacingare defined per token. iOSTheme.Fontsonly defines size and weight. - Shapes are more expressive — 11 named shape tokens including Material3 scale, vs iOS's 9 with some hardcoded magic numbers.
- White-label via build variant — replacing
Colors.ktandLocalShapes.ktinsrc/openedx/is cleaner than iOS's runtimeupdate()call.
What's missing
- No spacing scale — same gap as iOS; all padding is magic numbers at the call site
- No elevation tokens — shadow is not tokenized at all (not even a color token, unlike iOS's
shadowColor) headlineBoldbreaks the type scale — non-standard token at 34sp/Bold doesn't fit the M3 vocabulary; creates inconsistency with iOS- Some primitive aliases are already semantic —
light_text_accent = light_primaryinColors.ktmeans the primitive layer is partially semantic already, which muddies the separation - No line-height tokens on iOS counterpart — cross-platform typography diverges: Android has complete
TextStyles; iOS only has size + weight. This makes pixel-perfect parity harder to verify.
Platform Comparison Snapshot
| Dimension | iOS | Android |
|---|---|---|
| Primitive color layer | ✗ None | ✓ Colors.kt (hex vals) |
| Semantic color layer | ✓ Theme.Colors (static) | ✓ AppColors (data class) |
| Dark mode | Asset Catalog light/dark pairs | LightColorPalette / DarkColorPalette |
| Color count | ~91 | ~80 semantic + M3 delegates |
| White-label colors | Runtime update() | Build-time source set swap |
| Typography completeness | Size + weight only | Size + weight + lineHeight + letterSpacing |
| Type scale tokens | 16 | 17 (headlineBold extra) |
| Shape tokens | 9 | 11 (+ M3 scale of 5) |
| Spacing tokens | ✗ None | ✗ None |
| Theme access pattern | Theme.Colors.textPrimary (static) | MaterialTheme.appColors.textPrimary (CompositionLocal) |
Files Referenced
| File | Purpose |
|---|---|
core/src/main/…/theme/Theme.kt | OpenEdXTheme composable, LightColorPalette / DarkColorPalette assembly |
core/src/main/…/theme/AppColors.kt | Semantic color data class + M3 ColorScheme wrapper |
core/src/main/…/theme/AppTypography.kt | Typography data class + LocalTypography with defaults |
core/src/main/…/theme/AppShapes.kt | Shape data class definition |
core/src/openedx/…/theme/Colors.kt | Primitive color values (white-label flavor source set) |
core/src/openedx/…/theme/LocalShapes.kt | Shape instantiation with dp values (white-label flavor source set) |