Skip to content

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 values

Access pattern in Compose UI code:

kotlin
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       // CornerBasedShape

Theme injection (Composition Local):

kotlin
// In OpenEdXTheme composable:
CompositionLocalProvider(
    LocalAppColors provides colors,   // AppColors instance (light or dark)
    ...
)
// Each composable reads: LocalAppColors.current

Token Levels: What Exists vs. What's Missing

Token LevelColorsTypographySpacingShapes
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 presentPartial

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.

kotlin
val light_primary = Color(0xFF3C68FF)
val light_text_accent = light_primary          // aliases primitive
val light_primary_button_background = light_primary  // aliases primitive

Layer 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.

kotlin
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

PrimitiveLightDark
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

PrimitiveLightDark
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

TokenNotes
textPrimaryPrimary body text
textPrimaryVariantSlightly muted primary text
textPrimaryLightLight variant (= textPrimary in both modes)
textSecondaryMuted/secondary text
textDarkDark text (inverts in dark mode → white)
textAccentAccent-colored text (= primary)
textWarningWarning-state text
textHyperLinkLink text (= primary)

Text Fields

TokenNotes
textFieldBackgroundFocused field background
textFieldBackgroundVariantVariant (white in light, same surface in dark)
textFieldBorderField border
textFieldTextInput text color
textFieldHintPlaceholder/hint text

Primary Buttons

TokenNotes
primaryButtonBackgroundFilled button BG
primaryButtonTextFilled button label
primaryButtonBorderButton border (outlined variant)
primaryButtonBorderedTextOutlined button label

Secondary Buttons

TokenNotes
secondaryButtonBackground= primaryButtonBackground by default
secondaryButtonText= primaryButtonText by default
secondaryButtonBorder= primaryButtonBorder by default
secondaryButtonBorderedBackgroundOutlined secondary BG
secondaryButtonBorderedTextOutlined secondary label
inactiveButtonBackgroundDisabled button BG
inactiveButtonTextDisabled button text (= primaryButtonText)

Note: A comment in AppColors.kt explicitly 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

TokenNotes
cardViewBackgroundCard surface
cardViewBorderCard border
dividerHorizontal rule / divider
bottomSheetToggleBottom sheet drag handle
courseHomeHeaderShadeHeader overlay shade
courseHomeBackBtnBackgroundBack button BG in course header

Semantic Status

TokenNotes
warningWarning state (#FFC94D yellow)
onWarningOn-warning text (white)
infoInfo state (#3A9AE9 blue)
infoVariantInfo variant (= primary in light)
onInfoOn-info text (white)
successGreenSuccess green (#198571)
successBackgroundSuccess background
rateStarsStar rating yellow
certificateForegroundCertificate illustration

Progress / Course

TokenNotes
progressBarColor= successGreen
progressBarBackgroundColorProgress bar track
gradeProgressBarBorderGrade bar border
gradeProgressBarBackgroundGrade bar track
componentHorizontalProgressCompletedAndSelected#30A171 green
componentHorizontalProgressCompleted#BBE6D7 light green
componentHorizontalProgressSelected#F0CB00 yellow
componentHorizontalProgressDefault#D6D3D1 gray
assignmentCardBorderAssignment item border

Course Dates Timeline

TokenNotes
datesSectionBarPastDue= warning (yellow)
datesSectionBarToday= info (blue)
datesSectionBarThisWeek#3D4964 / #8E9BAE
datesSectionBarNextWeek#97A5BB / #4E5A70
datesSectionBarUpcoming#CCD4E0 / #273346

Tab Bar

TokenNotes
tabUnselectedBtnBackgroundUnselected tab BG
tabUnselectedBtnContentUnselected tab icon/text
tabSelectedBtnContentSelected tab icon/text

Auth

TokenNotes
authSSOSuccessBackgroundSSO success banner
authGoogleButtonBackgroundGoogle button
authFacebookButtonBackgroundFacebook button (#0866FF)
authMicrosoftButtonBackgroundMicrosoft button (near-black)

Settings

TokenNotes
settingsTitleContentSettings 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:

kotlin
@Deprecated val primaryVariant  // → primaryContainer
@Deprecated val secondaryVariant // → secondaryContainer

Typography

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):

WeightResource
FontWeight.ThinR.font.thin
FontWeight.LightR.font.extra_light, R.font.light
FontWeight.NormalR.font.regular
FontWeight.MediumR.font.medium
FontWeight.BoldR.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.

TokenSizeLine HeightWeightLetter Spacing
displayLarge57sp64spNormal-0.25sp
displayMedium45sp52spNormal0
displaySmall36sp44spBold0
headlineLarge32sp40spNormal0
headlineBold ⚠️34sp24spBold0
headlineMedium28sp36spNormal0
headlineSmall24sp32spSemiBold0
titleLarge22sp28spBold0
titleMedium16sp24spSemiBold0.1sp
titleSmall14sp20spMedium0.1sp
bodyLarge16sp24spNormal0.5sp
bodyMedium14sp20spNormal0.25sp
bodySmall12sp16spNormal0.4sp
labelLarge14sp20spMedium0.1sp
labelMedium12sp16spNormal0.5sp
labelSmall10sp16spNormal0

⚠️ headlineBold (34sp/Bold) is a non-standard addition not present in the iOS scale. It sits between headlineLarge (32sp) and displaySmall (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 TokenValue
extraSmall4dp
small8dp
medium12dp
large16dp
extraLarge24dp

Custom shape tokens:

TokenValueNotes
buttonShape8dp all cornersPrimary/secondary buttons
navigationButtonShape8dp all cornersNav bar buttons
textFieldShape8dp all cornersInput fields
cardShape12dp all cornersContent cards
sectionCardShape6dp all cornersSection-level cards
courseImageShape8dp all cornersCourse thumbnail
videoPreviewShape8dp all cornersVideo preview
screenBackgroundShape30dp top-start + top-end onlySheet-style screen overlay
screenBackgroundShapeFull24dp all cornersFull-screen rounded background
dialogShape24dp all cornersModal 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 layerColors.kt in a flavor source set gives brand swaps a single point of edit. iOS buries raw values in binary .colorset files.
  • 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 integrationAppColors wraps ColorScheme directly, so M3 components get themed automatically without manual wiring.
  • Typography is more completelineHeight and letterSpacing are defined per token. iOS Theme.Fonts only 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.kt and LocalShapes.kt in src/openedx/ is cleaner than iOS's runtime update() 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)
  • headlineBold breaks the type scale — non-standard token at 34sp/Bold doesn't fit the M3 vocabulary; creates inconsistency with iOS
  • Some primitive aliases are already semanticlight_text_accent = light_primary in Colors.kt means 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

DimensioniOSAndroid
Primitive color layer✗ NoneColors.kt (hex vals)
Semantic color layerTheme.Colors (static)AppColors (data class)
Dark modeAsset Catalog light/dark pairsLightColorPalette / DarkColorPalette
Color count~91~80 semantic + M3 delegates
White-label colorsRuntime update()Build-time source set swap
Typography completenessSize + weight onlySize + weight + lineHeight + letterSpacing
Type scale tokens1617 (headlineBold extra)
Shape tokens911 (+ M3 scale of 5)
Spacing tokens✗ None✗ None
Theme access patternTheme.Colors.textPrimary (static)MaterialTheme.appColors.textPrimary (CompositionLocal)

Files Referenced

FilePurpose
core/src/main/…/theme/Theme.ktOpenEdXTheme composable, LightColorPalette / DarkColorPalette assembly
core/src/main/…/theme/AppColors.ktSemantic color data class + M3 ColorScheme wrapper
core/src/main/…/theme/AppTypography.ktTypography data class + LocalTypography with defaults
core/src/main/…/theme/AppShapes.ktShape data class definition
core/src/openedx/…/theme/Colors.ktPrimitive color values (white-label flavor source set)
core/src/openedx/…/theme/LocalShapes.ktShape instantiation with dp values (white-label flavor source set)

Schema Education — Internal Research