Phase 2 · Building Interactive UIsModule 14~28 min read

Styling, Theming & Dark Mode

Craft a custom theme, support dark mode, and use dynamic color (Material You).

What you'll learn

A theme is what turns a functional app into your app. Because every component reads from MaterialTheme tokens, defining a theme once restyles the entire app — and makes dark mode almost automatic. We'll build a custom theme, add a dark scheme, and switch on Material You's dynamic color.

By the end you'll be able to:

  • Define a custom color scheme, typography, and shapes
  • Wrap your app in a reusable AppTheme
  • Support dark mode that follows the system setting
  • Enable dynamic color (Material You) on Android 12+

A custom MaterialTheme

A theme is three things: a color scheme, a typography set, and a shapes set. New projects come with a ui/theme/ folder holding exactly these files. You feed them into MaterialTheme, wrapped in your own composable — by convention AppTheme — so you configure it in one place:

Theme.kt
@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),   // follow the OS setting
    content: @Composable () -> Unit,
) {
    val colors = if (darkTheme) DarkColors else LightColors
    MaterialTheme(
        colorScheme = colors,
        typography = AppTypography,
        shapes = AppShapes,
        content = content,
    )
}

Then wrap your whole UI in it, once, from MainActivity:

MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AppTheme {                 // wrap the whole app once
                HomeScreen()           // every composable inside reads these tokens
            }
        }
    }
}

Key idea

Because every composable reads colors and type from the theme, this single wrap decides how the entire app looks. That's the reward for the discipline in Module 12: never hard-code a color or font size — always read from the theme.

Light & dark schemes

Compose gives you two builders — lightColorScheme() and darkColorScheme() — each taking the full set of roles (primary, onPrimary, background, surface, and so on). You define both; Compose picks the right one at runtime:

Color.kt
private val LightColors = lightColorScheme(
    primary = Color(0xFF6366F1),
    onPrimary = Color.White,
    secondary = Color(0xFF8B5CF6),
    background = Color(0xFFFDFDFF),
    surface = Color(0xFFF5F5FA),
)

private val DarkColors = darkColorScheme(
    primary = Color(0xFFA5B4FC),
    onPrimary = Color(0xFF1E1B4B),
    secondary = Color(0xFFC4B5FD),
    background = Color(0xFF0B0B12),
    surface = Color(0xFF14141F),
)

Tip

Dark colors aren't just "the light ones inverted." Use darker, desaturated surfaces and lighter, softer accents so nothing glares. Google's free Material Theme Builder tool generates a complete, accessible pair from a single seed color — a great starting point.

Supporting dark mode

The magic line is isSystemInDarkTheme() — a composable that returns whether the device is in dark mode. Feed it into your theme (as the darkTheme default) and your app follows the system setting with zero extra work. Because your components use token pairs like surface / onSurface, they recolor themselves correctly in both modes:

9:41
Good morning
3 tasks today
Start
Light — how this renders on a device
9:41
Good morning
3 tasks today
Start
Dark — how this renders on a device

Watch out

Always test both modes. A hard-coded Color.Black text will vanish on a dark background — the classic dark-mode bug. If something looks wrong in dark mode, hunt for a hard-coded color and replace it with the right colorScheme token.

Dynamic color (Material You)

On Android 12+, Material You can generate your app's color scheme from the user's wallpaper, so your app harmonizes with their personalized system colors. Use dynamicLightColorScheme() / dynamicDarkColorScheme(), guarded by a version check, and fall back to your brand colors on older devices:

Theme.kt
@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,     // Material You, Android 12+
    content: @Composable () -> Unit,
) {
    val context = LocalContext.current
    val colors = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ->
            if (darkTheme) dynamicDarkColorScheme(context)
            else dynamicLightColorScheme(context)
        darkTheme -> DarkColors
        else -> LightColors
    }
    MaterialTheme(colorScheme = colors, content = content)
}

Dynamic color is a lovely touch, but keep a strong branded fallback — and remember that your carefully chosen palette gets replaced when it's on. Many apps make it a user setting.

Recap & quick check

Key takeaways

  • A theme = color scheme + typography + shapes, fed into MaterialTheme via your own AppTheme composable.
  • Wrap the whole app in AppTheme once in setContent; every composable then reads the same tokens.
  • Define both lightColorScheme() and darkColorScheme(); pick between them with isSystemInDarkTheme().
  • Token pairs (surface/onSurface) make dark mode work automatically — never hard-code colors.
  • Dynamic color (Material You, Android 12+) derives the scheme from the wallpaper, with a branded fallback.

Quick check

1. What three token sets make up a Compose theme?

2. How does your app follow the system's dark mode setting?

3. Why does dark mode 'just work' when you use theme tokens?

4. Text disappears in dark mode. Likely cause?

5. What is dynamic color (Material You)?

That completes Phase 2 — you can build beautiful, interactive, themed screens! 🎉 In Phase 3 we connect screens together and give the app a real architecture, starting with Navigation in Compose.