Android Platform Design Guidelines โ Material Design 3
1. Material You & Theming [CRITICAL]
1.1 Dynamic Color
Enable dynamic color derived from the user's wallpaper. Dynamic color is the default on Android 12+ and should be the primary theming strategy.
@Composable
fun AppTheme(
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 = AppTypography,
content = content
)
}
<style name="Theme.App" parent="Theme.Material3.DayNight.NoActionBar">
<item name="dynamicColorThemeOverlay">@style/ThemeOverlay.Material3.DynamicColors.DayNight</item>
</style>
Rules:
- R1.1: Always provide a fallback static color scheme for devices below Android 12.
- R1.2: Never hardcode color hex values in components. Always reference color roles from the theme.
- R1.3: Test with at least 3 different wallpapers to verify dynamic color harmony.
1.2 Color Roles
Material 3 defines a structured set of color roles. Use them semantically, not aesthetically.
| Role |
Usage |
On-Role |
primary |
Key actions, active states, FAB |
onPrimary |
primaryContainer |
Less prominent primary elements |
onPrimaryContainer |
secondary |
Supporting UI, filter chips |
onSecondary |
secondaryContainer |
Navigation bar active indicator |
onSecondaryContainer |
tertiary |
Accent, contrast, complementary |
onTertiary |
tertiaryContainer |
Input fields, less prominent accents |
onTertiaryContainer |
surface |
Backgrounds, cards, sheets |
onSurface |
surfaceVariant |
Decorative elements, dividers |
onSurfaceVariant |
error |
Error states, destructive actions |
onError |
errorContainer |
Error backgrounds |
onErrorContainer |
outline |
Borders, dividers |
โ |
outlineVariant |
Subtle borders |
โ |
inverseSurface |
Snackbar background |
inverseOnSurface |
Text(
text = "Error message",
color = MaterialTheme.colorScheme.error
)
Surface(color = MaterialTheme.colorScheme.errorContainer) {
Text(text = "Error detail", color = MaterialTheme.colorScheme.onErrorContainer)
}
Text(text = "Error", color = Color(0xFFB00020))
Rules:
- R1.4: Every foreground element must use the matching
on color role for its background (e.g., onPrimary text on primary background).
- R1.5: Use
surface and its variants for backgrounds. Never use primary or secondary as large background areas.
- R1.6: Use
tertiary sparingly for accent and complementary contrast only.
1.3 Light and Dark Themes
Support both light and dark themes. Respect the system setting by default.
val darkTheme = isSystemInDarkTheme()
Rules:
- R1.7: Always support both light and dark themes. Never ship light-only.
- R1.8: Dark theme surfaces use elevation-based tonal mapping, not pure black (#000000). Use
surface color roles which handle this automatically.
- R1.9: Provide a manual theme override in app settings (System / Light / Dark).
1.4 Custom Color Seeds
When branding requires custom colors, provide a seed color and generate tonal palettes using Material Theme Builder.
private val BrandLightColorScheme = lightColorScheme(
primary = Color(0xFF1B6D2F),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFA4F6A8),
onPrimaryContainer = Color(0xFF002107),
)
Rules:
- R1.10: Generate tonal palettes from seed colors using Material Theme Builder. Never manually pick individual tones.
- R1.11: When using custom colors, still support dynamic color as the default and use custom colors as fallback.
2. Navigation [CRITICAL]
2.1 Navigation Bar (Bottom)
The primary navigation pattern for phones with 3-5 top-level destinations.
NavigationBar {
items.forEachIndexed { index, item ->
NavigationBarItem(
icon = {
Icon(
imageVector = if (selectedItem == index) item.filledIcon else item.outlinedIcon,
contentDescription = item.label
)
},
label = { Text(item.label) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
Rules:
- R2.1: Use Navigation Bar for 3-5 top-level destinations on compact screens. Never use for fewer than 3 or more than 5.
- R2.2: Always show labels on navigation bar items. Icon-only navigation bars are not permitted.
- R2.3: Use filled icons for the selected state and outlined icons for unselected states.
- R2.4: The active indicator uses
secondaryContainer color. Do not override this.
2.2 Navigation Rail
For medium and expanded screens (tablets, foldables, desktop).
NavigationRail(
header = {
FloatingActionButton(
onClick = { },
containerColor = MaterialTheme.colorScheme.tertiaryContainer
) {
Icon(Icons.Default.Add, contentDescription = "Create")
}
}
) {
items.forEachIndexed { index, item ->
NavigationRailItem(
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
Rules:
- R2.5: Use Navigation Rail on medium (600-839dp) and expanded (840dp+) window sizes. Pair it with Navigation Bar on compact.
- R2.6: Optionally include a FAB in the rail header for the primary action.
- R2.7: Labels are optional on the rail but recommended for clarity.
2.3 Navigation Drawer
For 5+ destinations or complex navigation hierarchies, typically on expanded screens.
PermanentNavigationDrawer(
drawerContent = {
PermanentDrawerSheet {
Text("App Name", modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.titleMedium)
HorizontalDivider()
items.forEach { item ->
NavigationDrawerItem(
label = { Text