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.

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.
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.
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 defines text styles from displayLarge down to labelSmall, while Shapes controls the corners of components:
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.
All tokens are bound together in a single theme composable:
@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.
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:
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.
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.
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:
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.
As a team grows, tokens need to be standardized. Collect all values in a single object that can be used anywhere:
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.
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:
isSystemInDarkTheme connects the theme to the system mode.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.