Android Design Skill
Expert knowledge of Material Design 3 for Jetpack Compose applications. This skill enforces design excellence, accessibility compliance, and performance best practices.
Core Principles
1. Material Design 3 Foundation
- Use semantic tokens, not hardcoded values
- Apply dynamic color for Android 12+
- Follow M3 shape scale (4dp extraSmall to 28dp extraLarge)
- Use elevation levels (0dp to 5 levels)
2. Accessibility First
- Minimum 48x48dp touch targets
- 4.5:1 contrast ratio for normal text
- Proper
contentDescriptionon all interactive elements - Support
fontScaleup to 200%
3. Performance Stability
- Mark data classes
@Immutableor@Stable - Use
rememberandderivedStateOfcorrectly - Prefer
LazyColumnoverColumnfor lists - Provide stable keys for list items
Typography Scale (15 Styles)
| Token | Size | Line Height | Weight | Use Case |
|---|---|---|---|---|
displayLarge | 57sp | 64sp | 400 | Hero headlines |
displayMedium | 45sp | 52sp | 400 | Page titles |
displaySmall | 36sp | 44sp | 400 | Section headers |
headlineLarge | 32sp | 40sp | 400 | Card titles |
headlineMedium | 28sp | 36sp | 400 | Dialog titles |
headlineSmall | 24sp | 32sp | 400 | List headers |
titleLarge | 22sp | 28sp | 400 | App bar titles |
titleMedium | 16sp | 24sp | 500 | Tab labels |
titleSmall | 14sp | 20sp | 500 | Chip labels |
bodyLarge | 16sp | 24sp | 400 | Body text |
bodyMedium | 14sp | 20sp | 400 | Secondary text |
bodySmall | 12sp | 16sp | 400 | Captions |
labelLarge | 14sp | 20sp | 500 | Buttons |
labelMedium | 12sp | 16sp | 500 | Small buttons |
labelSmall | 11sp | 16sp | 500 | Badges |
Usage Pattern
// GOOD - Semantic tokens
Text(
text = "Welcome",
style = MaterialTheme.typography.headlineLarge
)
// BAD - Hardcoded values
Text(
text = "Welcome",
fontSize = 32.sp,
fontWeight = FontWeight.Normal
)
Color System
Key Colors (5 Primary)
- Primary - Brand color, interactive elements
- Secondary - Less prominent UI
- Tertiary - Accents, special elements
- Error - Error states
- Neutral - Surfaces, backgrounds
Tonal Palettes (13 Tones)
Each key color generates: 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100
Usage Pattern
// GOOD - Semantic tokens
Surface(color = MaterialTheme.colorScheme.surface) {
Text(
text = "Hello",
color = MaterialTheme.colorScheme.onSurface
)
}
// BAD - Hardcoded colors
Surface(color = Color(0xFFFAFAFA)) {
Text(
text = "Hello",
color = Color.Black
)
}
Dynamic Color (Android 12+)
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
} else {
if (darkTheme) DarkColorScheme
else LightColorScheme
}
Shape Scale
| Token | Radius | Use Case |
|---|---|---|
extraSmall | 4dp | Badges, chips |
small | 8dp | Small cards, buttons |
medium | 12dp | Cards, dialogs |
large | 16dp | Large sheets |
extraLarge | 28dp | FABs, full sheets |
Touch Targets
Requirements
- Minimum: 48x48dp (M3 requirement)
- Recommended: 48x48dp with 8dp spacing
- WCAG 2.2: 24x24 CSS px minimum (Level AA)
Implementation
// GOOD - Proper touch target
IconButton(
onClick = { /* action */ },
modifier = Modifier.minimumInteractiveComponentSize()
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Close dialog"
)
}
// BAD - No touch target guarantee
Icon(
imageVector = Icons.Default.Close,
modifier = Modifier
.size(24.dp)
.clickable { /* action */ }
)
Spacing Scale
Base unit: 4dp
| Size | dp | Use Case |
|---|---|---|
| xs | 4dp | Inline spacing |
| sm | 8dp | Related elements |
| md | 12dp | Group spacing |
| lg | 16dp | Section spacing |
| xl | 24dp | Major divisions |
| 2xl | 32dp | Page margins |
| 3xl | 48dp | Large gaps |
| 4xl | 64dp | Hero spacing |
DO / DON'T
State Management
DO: Hoist state to appropriate level
@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
val query by viewModel.searchQuery.collectAsState()
SearchBar(query = query, onQueryChange = viewModel::updateQuery)
}
DON'T: Create state in leaf composables
@Composable
fun SearchBar() {
var query by remember { mutableStateOf("") } // Can't test, can't share
TextField(value = query, onValueChange = { query = it })
}
Modifiers
DO: Create modifiers outside composable body
private val cardModifier = Modifier
.fillMaxWidth()
.padding(16.dp)
@Composable
fun MyCard() {
Card(modifier = cardModifier) { /* content */ }
}
DON'T: Create modifiers inside composition
@Composable
fun MyCard() {
Card(
modifier = Modifier // Creates new object every recomposition
.fillMaxWidth()
.padding(16.dp)
) { /* content */ }
}
Collections
DO: Use immutable collections for stable recomposition
@Immutable
data class UiState(
val items: ImmutableList<Item> = persistentListOf()
)
DON'T: Use mutable collections in state
data class UiState(
val items: List<Item> = emptyList() // Unstable!
)
LazyColumn Keys
DO: Provide stable keys
LazyColumn {
items(items, key = { it.id }) { item ->
ItemRow(item)
}
}
DON'T: Rely on index-based keys
LazyColumn {
items(items.size) { index -> // Breaks on reorder
ItemRow(items[index])
}
}
Severity Levels
When auditing, classify issues as:
| Level | Icon | Description | Example |
|---|---|---|---|
| Critical | 🔴 | Blocks accessibility or causes crashes | Missing contentDescription |
| Important | 🟠 | Violates M3 guidelines or hurts UX | Touch target < 48dp |
| Warning | 🟡 | Suboptimal but functional | Hardcoded color |
| Suggestion | 🔵 | Polish opportunities | Non-standard spacing |
References
For detailed specifications, see: