Lesson 53 of 83 intermediate

Material Design 3, Theming, Dark Mode & Dynamic Color

Master Material You — from colorScheme to wallpaper-extracted dynamic color and custom theming architecture

Open interactive version (quiz + challenge)

Real-world analogy

A theme is like a brand manual for a company. It defines the colors, fonts, and shapes used everywhere so that every designer and engineer makes consistent choices. Material Design 3 is the brand manual; MaterialTheme is the library that enforces it in code so you cannot accidentally use the wrong shade of blue.

What is it?

Material Design 3 (M3, Material You) is Google's design system for Android apps. In Jetpack Compose it is implemented via MaterialTheme, which provides a ColorScheme, Typography, and Shapes to all composables in its tree. Dynamic color (Android 12+) extracts a personalized palette from the user's wallpaper. Dark mode is supported by swapping ColorScheme objects based on system preference.

Real-world relevance

Google's own apps — Google Messages, Google Photos, Google Calendar — were the first to adopt Material You with dynamic color at Android 12's launch. Monzo Bank uses a custom color system layered over M3, with carefully tuned semantic color roles that map to their coral brand color. YouTube's dark mode uses M3's surface and surfaceVariant roles to create depth hierarchy without using elevation shadows.

Key points

Code example

// ── App theme with dynamic color and dark mode support ───────
@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context)
            else dynamicLightColorScheme(context)
        }
        darkTheme -> darkColorScheme(
            primary = Color(0xFF_D9FE06),
            onPrimary = Color(0xFF_12151A),
            secondary = Color(0xFF_A8C7FA),
            background = Color(0xFF_12151A),
            surface = Color(0xFF_1D1F25)
        )
        else -> lightColorScheme(
            primary = Color(0xFF_4A5900),
            onPrimary = Color(0xFF_FFFFFF),
            secondary = Color(0xFF_555F71),
            background = Color(0xFF_FAFCF5),
            surface = Color(0xFF_FFFFFF)
        )
    }

    val typography = Typography(
        titleLarge = TextStyle(
            fontFamily = FontFamily(Font(R.font.poppins_semibold)),
            fontWeight = FontWeight.SemiBold,
            fontSize = 22.sp,
            lineHeight = 28.sp
        ),
        bodyMedium = TextStyle(
            fontFamily = FontFamily(Font(R.font.poppins_regular)),
            fontWeight = FontWeight.Normal,
            fontSize = 14.sp,
            lineHeight = 20.sp
        )
    )

    MaterialTheme(
        colorScheme = colorScheme,
        typography = typography,
        shapes = Shapes(
            small = RoundedCornerShape(8.dp),
            medium = RoundedCornerShape(16.dp),
            large = RoundedCornerShape(24.dp)
        ),
        content = content
    )
}

Line-by-line walkthrough

  1. 1. AppTheme accepts darkTheme and dynamicColor parameters — composable callers can override them for preview, testing, or user preference
  2. 2. The outermost when block checks for dynamic color availability: SDK >= S (Android 12) is required — older devices fall back to static brand schemes
  3. 3. dynamicDarkColorScheme(context) and dynamicLightColorScheme(context) call into the system wallpaper engine — they extract five seed colors and generate a full 30-role scheme
  4. 4. darkColorScheme() and lightColorScheme() are named constructors that set M3 role defaults — you only override the roles relevant to your brand, the rest default to M3 system values
  5. 5. Color(0xFF_D9FE06) is a raw color token — it is used only inside the theme block, never in UI composables directly
  6. 6. Typography() lets you override any of the 15 type scale slots — unspecified slots inherit M3 defaults, so you only define what differs from the baseline
  7. 7. FontFamily(Font(R.font.poppins_semibold)) loads a bundled font resource — the downloadable fonts API (GoogleFont) avoids bundling by fetching from Google Fonts at runtime
  8. 8. Shapes() takes small, medium, and large — M3 maps them to specific component groups; Cards use medium, FABs use large, Chips use small
  9. 9. MaterialTheme wraps content and provides colorScheme, typography, and shapes as CompositionLocals — every child composable in content can read them via MaterialTheme.colorScheme etc.

Spot the bug

@Composable
fun StatusBadge(isOnline: Boolean) {
    val badgeColor = if (isOnline) Color(0xFF4CAF50) else Color(0xFFE53935)
    Box(
        modifier = Modifier
            .background(badgeColor)
            .padding(4.dp)
    ) {
        Text(
            text = if (isOnline) "Online" else "Offline",
            color = Color.White
        )
    }
}
Need a hint?
This works in light mode. What breaks in dark mode, and how does it violate the M3 theming contract?
Show answer
Three issues: (1) Hardcoded Color(0xFF4CAF50) and Color(0xFFE53935) bypass the M3 ColorScheme entirely. These colors are not checked against the surface they appear on — in dark mode with a dark surface, the contrast may be insufficient. Fix: define semantic color roles in the theme (e.g., a custom onlineColor and offlineColor token in a custom CompositionLocal, or map to M3 roles — green maps naturally to a tertiary container role, red to the error role). (2) Text color is hardcoded to Color.White — this fails on light backgrounds if the badge is placed on a white card in light mode. Fix: use MaterialTheme.colorScheme.onError for the text on the error (red) background, and MaterialTheme.colorScheme.onTertiaryContainer for the online badge. (3) The component will not adapt to dynamic color — if the user's wallpaper-derived M3 scheme uses a different primary hue, the hardcoded greens and reds will clash. Semantic color roles allow the design system to make coherent color decisions.

Explain like I'm 5

A theme is like choosing clothes for your whole app before it gets dressed each morning. Instead of every button picking its own color, you tell the whole app 'today we wear blue' (or neon green, or whatever your wallpaper suggests). Every button, card, and text automatically wears the right shade of that color, matching perfectly.

Fun fact

Material You's dynamic color algorithm is called 'Tonal Palette Extraction' and uses a quantization algorithm (similar to k-means clustering) to extract five key colors from the wallpaper, then generates a full 30-role ColorScheme from those five tones using the HCT (Hue, Chroma, Tone) color model — a perceptually uniform color space developed by Google specifically for this feature.

Hands-on challenge

Build a complete AppTheme composable that supports: (1) dynamic color on Android 12+ with a static brand fallback, (2) a user-toggleable dark/light mode preference stored in DataStore, (3) a custom Typography using a Google Font loaded with the downloadable fonts API. Apply the theme to a screen with at least one Card, one Button, and one TopAppBar, and add dark/light @Preview annotations.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery