ANDROID-ARCHITECT

AI-powered Android development assistant.

SYSONLINE
SDK36 · MIN 24
KOTLIN2.0
UTC07:37:53
UP00:01
MOD · ARTICLE · JETPACK COMPOSES/N · AX-JETPACPUBLISHED
Jetpack ComposeFeb 20, 202610 MIN

Building a Design System with Jetpack Compose

Build a scalable Compose design system: design tokens as the foundation, Material 3 extended with custom CompositionLocals, and an atomic component library.

By Rocky Elsalaymeh · Founder & Principal Consultant, Strategia-X

Why Your App Needs a Design System

A design system is the single source of truth for how your app looks, feels, and behaves. Without one, developers make ad-hoc styling decisions. Button padding varies between screens, text sizes drift, and color usage becomes inconsistent. This accumulates into a UI that feels unpolished and is expensive to maintain. Jetpack Compose makes building a design system significantly easier than the XML-based approach. Compose's composable functions, theme system, and Kotlin type safety let you define tokens, components, and patterns that enforce consistency at compile time.

Design Tokens: The Foundation

Design tokens are the atomic values of your visual language: colors, typography scales, spacing units, elevation levels, and corner radii. They are the building blocks that every component references. Changing a token value propagates across the entire app. In Compose, design tokens map to your theme's color scheme, typography, and shapes. Material 3's dynamic color system gives you a sophisticated token infrastructure out of the box, but most apps need custom extensions.
kotlin
// Custom color tokens beyond Material 3
@Immutable
data class ExtendedColors(
    val success: Color,
    val warning: Color,
    val info: Color,
    val onSuccess: Color,
    val onWarning: Color,
    val onInfo: Color,
    val surfaceVariantDim: Color,
    val borderSubtle: Color,
)

val LocalExtendedColors = staticCompositionLocalOf {
    ExtendedColors(
        success = Color(0xFF2E7D32),
        warning = Color(0xFFF9A825),
        info = Color(0xFF1565C0),
        onSuccess = Color.White,
        onWarning = Color.Black,
        onInfo = Color.White,
        surfaceVariantDim = Color(0xFF1A1C1E),
        borderSubtle = Color(0xFF2C2F33),
    )
}

// Spacing tokens using an 8-point grid
object Spacing {
    val xxxs = 2.dp   // Fine adjustment
    val xxs = 4.dp
    val xs = 8.dp
    val sm = 12.dp
    val md = 16.dp
    val lg = 24.dp
    val xl = 32.dp
    val xxl = 48.dp
    val xxxl = 64.dp
}

Building the Theme Provider

Your theme provider wraps Material 3's MaterialTheme and adds your custom tokens via CompositionLocal. This makes custom tokens accessible anywhere in the composable tree through a convenient accessor object.
kotlin
@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colorScheme = if (darkTheme) darkColorScheme(
        primary = Color(0xFFBB86FC),
        secondary = Color(0xFF03DAC5),
        surface = Color(0xFF121212),
    ) else lightColorScheme(
        primary = Color(0xFF6200EE),
        secondary = Color(0xFF03DAC5),
        surface = Color(0xFFFFFBFE),
    )

    val extendedColors = if (darkTheme) ExtendedColors(
        success = Color(0xFF66BB6A),
        warning = Color(0xFFFFCA28),
        info = Color(0xFF42A5F5),
        onSuccess = Color.Black,
        onWarning = Color.Black,
        onInfo = Color.Black,
        surfaceVariantDim = Color(0xFF1A1C1E),
        borderSubtle = Color(0xFF2C2F33),
    ) else ExtendedColors(
        success = Color(0xFF2E7D32),
        warning = Color(0xFFF9A825),
        info = Color(0xFF1565C0),
        onSuccess = Color.White,
        onWarning = Color.Black,
        onInfo = Color.White,
        surfaceVariantDim = Color(0xFFF5F5F5),
        borderSubtle = Color(0xFFE0E0E0),
    )

    CompositionLocalProvider(
        LocalExtendedColors provides extendedColors
    ) {
        MaterialTheme(
            colorScheme = colorScheme,
            typography = AppTypography,
            shapes = AppShapes,
            content = content,
        )
    }
}

// Convenient accessor
object AppTheme {
    val extendedColors: ExtendedColors
        @Composable get() = LocalExtendedColors.current
}

Component Library: Atoms to Organisms

Organize your components into layers following atomic design: - **Atoms**: Basic elements like AppButton, AppText, AppIcon, AppBadge - **Molecules**: Combinations like SearchBar (TextField + Icon + Clear button) - **Organisms**: Complex sections like UserProfileCard (Avatar + Name + Stats + Action buttons) Each component should accept only the parameters it needs, use your design tokens exclusively (never hardcoded values), and provide sensible defaults.
kotlin
// Atom: Button with consistent design language
@Composable
fun AppButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    variant: ButtonVariant = ButtonVariant.Primary,
    size: ButtonSize = ButtonSize.Medium,
    enabled: Boolean = true,
    leadingIcon: ImageVector? = null,
) {
    val colors = when (variant) {
        ButtonVariant.Primary -> ButtonDefaults.buttonColors(
            containerColor = MaterialTheme.colorScheme.primary,
            contentColor = MaterialTheme.colorScheme.onPrimary,
        )
        ButtonVariant.Secondary -> ButtonDefaults.outlinedButtonColors()
        ButtonVariant.Danger -> ButtonDefaults.buttonColors(
            containerColor = MaterialTheme.colorScheme.error,
            contentColor = MaterialTheme.colorScheme.onError,
        )
    }

    val padding = when (size) {
        ButtonSize.Small -> PaddingValues(
            horizontal = Spacing.sm, vertical = Spacing.xxs
        )
        ButtonSize.Medium -> PaddingValues(
            horizontal = Spacing.md, vertical = Spacing.xs
        )
        ButtonSize.Large -> PaddingValues(
            horizontal = Spacing.lg, vertical = Spacing.sm
        )
    }

    Button(
        onClick = onClick,
        modifier = modifier,
        enabled = enabled,
        colors = colors,
        contentPadding = padding,
        shape = AppShapes.medium,
    ) {
        if (leadingIcon != null) {
            Icon(
                imageVector = leadingIcon,
                contentDescription = null,
                modifier = Modifier.size(18.dp),
            )
            Spacer(Modifier.width(Spacing.xs))
        }
        Text(text = text)
    }
}

enum class ButtonVariant { Primary, Secondary, Danger }
enum class ButtonSize { Small, Medium, Large }

Previews and Documentation

Compose Previews serve as living documentation for your design system. Create preview functions for every component variant, size, and state. Group them in a dedicated preview file so designers and developers can reference the full catalog. For larger teams, consider generating a showcase app that displays every component with interactive controls. This becomes the team's visual reference and catches regressions when tokens change.
kotlin
@Preview(name = "Primary Button", group = "Buttons")
@Preview(
    name = "Primary Button - Dark",
    group = "Buttons",
    uiMode = Configuration.UI_MODE_NIGHT_YES
)
@Composable
private fun PrimaryButtonPreview() {
    AppTheme {
        AppButton(
            text = "Get Started",
            onClick = {},
            leadingIcon = Icons.Default.ArrowForward,
        )
    }
}

@Preview(name = "All Button Variants", group = "Buttons")
@Composable
private fun AllButtonsPreview() {
    AppTheme {
        Column(
            verticalArrangement = Arrangement.spacedBy(Spacing.sm)
        ) {
            ButtonVariant.entries.forEach { variant ->
                ButtonSize.entries.forEach { size ->
                    AppButton(
                        text = "${variant.name} ${size.name}",
                        onClick = {},
                        variant = variant,
                        size = size,
                    )
                }
            }
        }
    }
}
MOD · TAKEAWAYS5 POINTSSUMMARY

Key Takeaways

  1. 1Design tokens (colors, spacing, typography) are the atomic foundation of every component.
  2. 2Extend Material 3 with custom CompositionLocal tokens for app-specific needs.
  3. 3Follow atomic design: atoms, molecules, organisms for a scalable component library.
  4. 4Never hardcode values in components -- always reference design tokens.
  5. 5Compose Previews are living documentation for your design system.
MOD · FAQ3 ENTRIESANSWERED

Frequently Asked

What are design tokens in a Compose design system?

Tokens are the atomic values -- colors, spacing, typography -- that every component reads from. They are the foundation layer: components reference tokens and never hardcode a value.

How do I extend Material 3 with my own tokens?

Add custom CompositionLocal tokens alongside the Material 3 theme for app-specific needs, rather than forking or fighting the Material theme.

How should I structure a Compose component library?

Follow atomic design -- atoms, then molecules, then organisms -- so the library scales. Compose Previews then act as living documentation for each level.

MOD · BUILD · NEXTS/N · AX-CTA-0001READY

Ready to architect your next Android app?

ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.

ANDROID-ARCHITECT · CONSOLE
S/N · AX-1A-00001
STRATEGIA-X · ENGINEERED · IN · CALIFORNIA