Learn Jetpack Compose - Theming & Material Design
Episode 6 of 23

Learn Jetpack Compose - Theming & Material Design

This episode builds visual consistency through MaterialTheme: color scheme, typography, and shapes, light and dark theme support, dynamic color on Android 12, and design system tokenization for applications at scale.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

A polished application isn't born from randomly chosen colors. Behind every pleasant-looking screen there's a system: a consistent set of colors, typography, and shapes. In Compose, that system is called MaterialTheme.

All the Material 3 components you used in episode 4 read from the theme: buttons, text, and cards automatically pick up colors and shapes from it. Change one token and the whole application changes. That's the power of centralized theming.

Episode 6 covers the structure of MaterialTheme, light and dark support, dynamic color, and design system tokenization.

MaterialTheme: Colors, Typography, and Shapes

ColorScheme

Every Compose application has a Theme.kt file that defines the ColorScheme. Colors are not stored as loose values but as roles: primary, secondary, surface, background, and so on. Components take these roles, not raw colors.

KotlinLight ColorScheme
private val LightColors = lightColorScheme(
    primary = Color(0xFF3B5BDB),
    secondary = Color(0xFF5E81AC),
    surface = Color(0xFFFAFAFA),
    onPrimary = Color.White
)

lightColorScheme fills the color roles for light mode. By using roles, a branding change only needs to happen in one place — all buttons and surfaces adapt automatically.

Typography and Shapes

Typography defines text styles from displayLarge down to labelSmall, while Shapes controls the corners of components:

KotlinTypography and Shapes
val AppTypography = Typography(
    headlineMedium = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 28.sp,
        lineHeight = 36.sp
    )
)
 
val AppShapes = Shapes(
    small = RoundedCornerShape(8.dp),
    medium = RoundedCornerShape(12.dp),
    large = RoundedCornerShape(16.dp)
)

Text uses built-in styles like headlineMedium through MaterialTheme.typography, keeping the typographic hierarchy consistent across all screens.

Assembling the Theme

All tokens are bound together in a single theme composable:

KotlinMaterialTheme
@Composable
fun BelajarComposeTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colorScheme = if (darkTheme) DarkColors else LightColors
    MaterialTheme(
        colorScheme = colorScheme,
        typography = AppTypography,
        shapes = AppShapes,
        content = content
    )
}

MaterialTheme provides colors, typography, and shapes to the entire subtree. isSystemInDarkTheme() reads the system setting — the basis of dark mode support. To see the theme results on a device, build and install the application with ./gradlew :app:installDebug.

Light and Dark Theme

Two Sets of Colors

Dark mode is not simply inverted colors. Roles like surface and onSurface need to be rearranged so contrast is preserved on a dark background. Define DarkColors as the counterpart to LightColors:

KotlinDark color scheme
private val DarkColors = darkColorScheme(
    primary = Color(0xFF8FA8FF),
    secondary = Color(0xFF9BB4E8),
    surface = Color(0xFF121212),
    onPrimary = Color(0xFF1A1A2E)
)

darkColorScheme fills the roles for dark mode. With the pattern above, the application automatically switches themes when the system changes modes — no extra logic needed on each screen.

Respecting User Preferences

Sometimes users want to lock the theme, for example always dark. Store that preference in DataStore (episode 11) and pass the darkTheme value explicitly to BelajarComposeTheme as a parameter.

Dynamic Color and Material 3

Colors from the Wallpaper

On Android 12 and up, dynamic color takes colors from the user's wallpaper and applies them to the application. dynamicDarkColorScheme and dynamicLightColorScheme provide those schemes automatically:

KotlinDynamic color
val context = LocalContext.current
val colorScheme = if (dynamicColor && Build.VERSION.SDK_INT >= 31) {
    if (darkTheme) dynamicDarkColorScheme(context)
    else dynamicLightColorScheme(context)
} else {
    if (darkTheme) DarkColors else LightColors
}

dynamicDarkColorScheme(context) generates a scheme from the wallpaper. For devices below Android 12, you fall back to the static scheme. This feature provides a strong sense of personalization with very little code.

Design System Tokenization

Toward Your Own Design System

As a team grows, tokens need to be standardized. Collect all values in a single object that can be used anywhere:

KotlinDesign tokens
object Spacing {
    val xs = 4.dp
    val sm = 8.dp
    val md = 16.dp
    val lg = 24.dp
}
 
object Dimensi {
    val radiusCard = 16.dp
    val tinggiTopBar = 64.dp
}

Tokens like Spacing.md replace magic numbers across the codebase. Design changes only need to happen in this object — consistency and iteration speed rise together.

Closing

Episode 6 built a visual system: MaterialTheme with ColorScheme, Typography, and Shapes, light and dark theme support through isSystemInDarkTheme, dynamic color on Android 12 with dynamicLightColorScheme, and design system tokenization for applications at scale.

Key takeaways:

  • Colors are stored as roles in ColorScheme, not loose values.
  • Typography and Shapes unify text hierarchy and component corners.
  • isSystemInDarkTheme connects the theme to the system mode.
  • Dynamic color takes wallpaper colors on Android 12 and up.
  • Token objects replace magic numbers across the codebase.
  • MaterialTheme provides all tokens to the subtree at once.

In episode 7 we will discuss handling input and events — TextField, Button, Checkbox, and Switch, gesture detection, focus and keyboard, input validation, and Snackbar, dialogs, and bottom sheets.