Material 3 Theming in Compose: Building Dynamic Color Systems
Why Material 3 Changes Everything
Material 3 (also known as Material You) represents a fundamental shift in how Android apps approach theming. Instead of static color palettes, M3 introduces dynamic color extraction from user wallpapers, creating a personalized experience for every user.
For developers migrating from XML, this is a significant upgrade. Traditional theme.xml files with hardcoded colors are replaced by a flexible, programmatic color system that adapts to user preferences automatically.
Setting Up Material 3 in Compose
The foundation of M3 theming is the MaterialTheme composable. Unlike M2, M3 uses a ColorScheme with expanded color roles:
@Composable
fun MyAppTheme(
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
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}Understanding Color Roles
M3 introduces semantic color roles that describe the purpose of colors rather than their appearance:
- primary/onPrimary: Main brand color and text on top of it
- secondary/onSecondary: Supporting accent color for less prominent elements
- tertiary/onTertiary: Contrasting accent for visual interest
- surface/onSurface: Background colors and text for content areas
- error/onError: States indicating errors or destructive actions
Custom Color Schemes
For brand consistency, you can define custom color schemes while still supporting dynamic colors as a fallback:
private val LightColorScheme = lightColorScheme(
primary = Color(0xFF6750A4),
onPrimary = Color.White,
primaryContainer = Color(0xFFEADDFF),
onPrimaryContainer = Color(0xFF21005D),
secondary = Color(0xFF625B71),
// ... other colors
)
private val DarkColorScheme = darkColorScheme(
primary = Color(0xFFD0BCFF),
onPrimary = Color(0xFF381E72),
primaryContainer = Color(0xFF4F378B),
onPrimaryContainer = Color(0xFFEADDFF),
// ... other colors
)