On this page
Hard-coded Color(0xFF6200EE) scattered across screens looks fine until you add dark mode, rebrand, or test contrast. Then every screen needs edits. A well-built Material 3 theme fixes this once: components read roles like primary and surface, and the theme decides the actual colours.
The three pillars of a Material 3 theme
Building the theme
Generate a palette from your brand colour with the Material Theme Builder, which exports Compose code for light and dark schemes. Then wire it up:
private val LightColors = lightColorScheme(
primary = Color(0xFF0061A4),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFD1E4FF),
onPrimaryContainer = Color(0xFF001D36),
// ... remaining roles exported from Material Theme Builder
)
private val DarkColors = darkColorScheme(
primary = Color(0xFF9ECAFF),
onPrimary = Color(0xFF003258),
primaryContainer = Color(0xFF00497D),
onPrimaryContainer = Color(0xFFD1E4FF),
// ...
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit,
) {
val context = LocalContext.current
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ->
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
darkTheme -> DarkColors
else -> LightColors
}
MaterialTheme(colorScheme = colorScheme, typography = AppTypography, shapes = AppShapes, content = content)
}Dynamic color: when to use it
On Android 12 (API 31) and later, dynamic color derives a scheme from the user's wallpaper, making your app feel native to their device. It's not always right:
- Use it for utility and personal apps where blending with the system is a plus.
- Make it optional or skip it where brand recognition matters, such as banking, retail or media brands. You can still apply dynamic color only to neutral surfaces.
- Always provide a fallback scheme for older devices.
Typography and shapes
val Brand = FontFamily(Font(R.font.inter_regular), Font(R.font.inter_semibold, FontWeight.SemiBold))
val AppTypography = Typography().run {
copy(
headlineSmall = headlineSmall.copy(fontFamily = Brand, fontWeight = FontWeight.SemiBold),
titleMedium = titleMedium.copy(fontFamily = Brand, fontWeight = FontWeight.SemiBold),
bodyLarge = bodyLarge.copy(fontFamily = Brand),
labelLarge = labelLarge.copy(fontFamily = Brand, fontWeight = FontWeight.SemiBold),
)
}
val AppShapes = Shapes(
small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(12.dp),
large = RoundedCornerShape(20.dp),
)Using theme values in components
Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh),
shape = MaterialTheme.shapes.large,
) {
Text("Order #1042", style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface)
Text("Out for delivery", style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant)
}Always pair a container with its "on" role (primary with onPrimary, surface with onSurface). Material 3's tonal palettes are designed so these pairs meet contrast guidelines.
Extending the theme with your own tokens
@Immutable
data class ExtendedColors(val success: Color, val onSuccess: Color, val warning: Color)
val LocalExtendedColors = staticCompositionLocalOf {
ExtendedColors(success = Color.Unspecified, onSuccess = Color.Unspecified, warning = Color.Unspecified)
}
// In AppTheme:
// CompositionLocalProvider(LocalExtendedColors provides if (darkTheme) DarkExtended else LightExtended) {
// MaterialTheme(colorScheme = colorScheme, typography = AppTypography, shapes = AppShapes, content = content)
// }
object AppTheme {
val extendedColors: ExtendedColors
@Composable get() = LocalExtendedColors.current
}
// Usage: Text("Paid", color = AppTheme.extendedColors.success)Real-world setups
Brand-first app
Fixed brand schemes for light and dark, with dynamic color disabled. Semantic success and warning tokens via CompositionLocal.
Personal utility app
Dynamic color on by default, with a settings toggle and a brand fallback.
Multi-brand white-label
Build one theme function per brand from the same token structure. Screens never change.
Common mistakes
- Hard-coding colours and text sizes in components.
- Using
Color.Whitetext onprimary, which breaks in dark mode or with dynamic color. UseonPrimary. - Forgetting to theme the system bars and the window background, which causes flashes on startup.
- Not testing large font scale and contrast. Run Android Studio's accessibility checks and try TalkBack.
Frequently asked questions
Should I still define an XML theme?
Yes, a minimal one for the Activity window (background, splash and system bar defaults), even if all UI is in Compose.
Can users choose light or dark in the app?
Yes. Store the preference in DataStore and pass darkTheme into your theme instead of relying only on isSystemInDarkTheme().
Is Material 3 required?
No. You can build a fully custom design system on Compose foundation, but Material 3 gives you accessible, well-tested defaults.
Conclusion
A solid Material 3 theme defines color roles, typography and shapes once, supports dark mode and optional dynamic color, and extends cleanly with your own semantic tokens. Components that read roles instead of raw values make rebrands and accessibility fixes painless.
Next step: keep that polished UI fast with Compose Performance Optimization.
Official references
📱 Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it ▸
NotiVaultNotification history: recover dismissed notificationsGet it ▸
Ready to build real Android applications?
Jetpack Compose clicks once you build real screens with real state. Practise modern Android UI development on projects that go beyond toy examples.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.