Material Design 3 and Android platform guidelines. Use when building Android apps with Jetpack Compose or XML layouts, implementing Material You, navigation, or accessibility. Triggers on tasks involving Android UI, Compose components, dynamic color, or Material Design compliance.
Add this skill
npx mdskills install ehmo/androidComprehensive Material Design 3 guidelines with clear rules, semantic color usage, and adaptive layout patterns
1---2name: android-design-guidelines3description: Material Design 3 and Android platform guidelines. Use when building Android apps with Jetpack Compose or XML layouts, implementing Material You, navigation, or accessibility. Triggers on tasks involving Android UI, Compose components, dynamic color, or Material Design compliance.4license: MIT5metadata:6 author: platform-design-skills7 version: "1.0.0"8---910# Android Platform Design Guidelines — Material Design 31112## 1. Material You & Theming [CRITICAL]1314### 1.1 Dynamic Color1516Enable dynamic color derived from the user's wallpaper. Dynamic color is the default on Android 12+ and should be the primary theming strategy.1718```kotlin19// Compose: Dynamic color theme20@Composable21fun AppTheme(22 darkTheme: Boolean = isSystemInDarkTheme(),23 dynamicColor: Boolean = true,24 content: @Composable () -> Unit25) {26 val colorScheme = when {27 dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {28 val context = LocalContext.current29 if (darkTheme) dynamicDarkColorScheme(context)30 else dynamicLightColorScheme(context)31 }32 darkTheme -> darkColorScheme()33 else -> lightColorScheme()34 }35 MaterialTheme(36 colorScheme = colorScheme,37 typography = AppTypography,38 content = content39 )40}41```4243```xml44<!-- XML: Dynamic color in themes.xml -->45<style name="Theme.App" parent="Theme.Material3.DayNight.NoActionBar">46 <item name="dynamicColorThemeOverlay">@style/ThemeOverlay.Material3.DynamicColors.DayNight</item>47</style>48```4950**Rules:**51- R1.1: Always provide a fallback static color scheme for devices below Android 12.52- R1.2: Never hardcode color hex values in components. Always reference color roles from the theme.53- R1.3: Test with at least 3 different wallpapers to verify dynamic color harmony.5455### 1.2 Color Roles5657Material 3 defines a structured set of color roles. Use them semantically, not aesthetically.5859| Role | Usage | On-Role |60|------|-------|---------|61| `primary` | Key actions, active states, FAB | `onPrimary` |62| `primaryContainer` | Less prominent primary elements | `onPrimaryContainer` |63| `secondary` | Supporting UI, filter chips | `onSecondary` |64| `secondaryContainer` | Navigation bar active indicator | `onSecondaryContainer` |65| `tertiary` | Accent, contrast, complementary | `onTertiary` |66| `tertiaryContainer` | Input fields, less prominent accents | `onTertiaryContainer` |67| `surface` | Backgrounds, cards, sheets | `onSurface` |68| `surfaceVariant` | Decorative elements, dividers | `onSurfaceVariant` |69| `error` | Error states, destructive actions | `onError` |70| `errorContainer` | Error backgrounds | `onErrorContainer` |71| `outline` | Borders, dividers | — |72| `outlineVariant` | Subtle borders | — |73| `inverseSurface` | Snackbar background | `inverseOnSurface` |7475```kotlin76// Correct: semantic color roles77Text(78 text = "Error message",79 color = MaterialTheme.colorScheme.error80)81Surface(color = MaterialTheme.colorScheme.errorContainer) {82 Text(text = "Error detail", color = MaterialTheme.colorScheme.onErrorContainer)83}8485// WRONG: hardcoded colors86Text(text = "Error", color = Color(0xFFB00020)) // Anti-pattern87```8889**Rules:**90- R1.4: Every foreground element must use the matching `on` color role for its background (e.g., `onPrimary` text on `primary` background).91- R1.5: Use `surface` and its variants for backgrounds. Never use `primary` or `secondary` as large background areas.92- R1.6: Use `tertiary` sparingly for accent and complementary contrast only.9394### 1.3 Light and Dark Themes9596Support both light and dark themes. Respect the system setting by default.9798```kotlin99// Compose: Detect system theme100val darkTheme = isSystemInDarkTheme()101```102103**Rules:**104- R1.7: Always support both light and dark themes. Never ship light-only.105- R1.8: Dark theme surfaces use elevation-based tonal mapping, not pure black (#000000). Use `surface` color roles which handle this automatically.106- R1.9: Provide a manual theme override in app settings (System / Light / Dark).107108### 1.4 Custom Color Seeds109110When branding requires custom colors, provide a seed color and generate tonal palettes using Material Theme Builder.111112```kotlin113// Custom color scheme with brand seed114private val BrandLightColorScheme = lightColorScheme(115 primary = Color(0xFF1B6D2F),116 onPrimary = Color(0xFFFFFFFF),117 primaryContainer = Color(0xFFA4F6A8),118 onPrimaryContainer = Color(0xFF002107),119 // ... generate full palette from seed120)121```122123**Rules:**124- R1.10: Generate tonal palettes from seed colors using Material Theme Builder. Never manually pick individual tones.125- R1.11: When using custom colors, still support dynamic color as the default and use custom colors as fallback.126127---128129## 2. Navigation [CRITICAL]130131### 2.1 Navigation Bar (Bottom)132133The primary navigation pattern for phones with 3-5 top-level destinations.134135```kotlin136// Compose: Navigation Bar137NavigationBar {138 items.forEachIndexed { index, item ->139 NavigationBarItem(140 icon = {141 Icon(142 imageVector = if (selectedItem == index) item.filledIcon else item.outlinedIcon,143 contentDescription = item.label144 )145 },146 label = { Text(item.label) },147 selected = selectedItem == index,148 onClick = { selectedItem = index }149 )150 }151}152```153154**Rules:**155- R2.1: Use Navigation Bar for 3-5 top-level destinations on compact screens. Never use for fewer than 3 or more than 5.156- R2.2: Always show labels on navigation bar items. Icon-only navigation bars are not permitted.157- R2.3: Use filled icons for the selected state and outlined icons for unselected states.158- R2.4: The active indicator uses `secondaryContainer` color. Do not override this.159160### 2.2 Navigation Rail161162For medium and expanded screens (tablets, foldables, desktop).163164```kotlin165// Compose: Navigation Rail for larger screens166NavigationRail(167 header = {168 FloatingActionButton(169 onClick = { /* primary action */ },170 containerColor = MaterialTheme.colorScheme.tertiaryContainer171 ) {172 Icon(Icons.Default.Add, contentDescription = "Create")173 }174 }175) {176 items.forEachIndexed { index, item ->177 NavigationRailItem(178 icon = { Icon(item.icon, contentDescription = item.label) },179 label = { Text(item.label) },180 selected = selectedItem == index,181 onClick = { selectedItem = index }182 )183 }184}185```186187**Rules:**188- R2.5: Use Navigation Rail on medium (600-839dp) and expanded (840dp+) window sizes. Pair it with Navigation Bar on compact.189- R2.6: Optionally include a FAB in the rail header for the primary action.190- R2.7: Labels are optional on the rail but recommended for clarity.191192### 2.3 Navigation Drawer193194For 5+ destinations or complex navigation hierarchies, typically on expanded screens.195196```kotlin197// Compose: Permanent Navigation Drawer for large screens198PermanentNavigationDrawer(199 drawerContent = {200 PermanentDrawerSheet {201 Text("App Name", modifier = Modifier.padding(16.dp),202 style = MaterialTheme.typography.titleMedium)203 HorizontalDivider()204 items.forEach { item ->205 NavigationDrawerItem(206 label = { Text(item.label) },207 selected = item == selectedItem,208 onClick = { selectedItem = item },209 icon = { Icon(item.icon, contentDescription = null) }210 )211 }212 }213 }214) {215 Scaffold { /* page content */ }216}217```218219**Rules:**220- R2.8: Use modal drawer on compact screens, permanent drawer on expanded screens.221- R2.9: Group drawer items into sections with dividers and section headers.222223### 2.4 Predictive Back Gesture224225Android 13+ supports predictive back with an animation preview.226227```kotlin228// Compose: Predictive back handling229val predictiveBackHandler = remember { PredictiveBackHandler(enabled = true) { progress ->230 // Animate based on progress (0.0 to 1.0)231}}232```233234```xml235<!-- AndroidManifest.xml: opt in to predictive back -->236<application android:enableOnBackInvokedCallback="true">237```238239**Rules:**240- R2.10: Opt in to predictive back in the manifest. Handle `OnBackInvokedCallback` instead of overriding `onBackPressed()`.241- R2.11: The system back gesture navigates back in the navigation stack. The Up button (toolbar arrow) navigates up in the app hierarchy. These may differ.242- R2.12: Never intercept system back to show "are you sure?" dialogs unless there is unsaved user input.243244### 2.5 Navigation Component Selection245246| Screen Size | 3-5 Destinations | 5+ Destinations |247|-------------|-------------------|-----------------|248| Compact (< 600dp) | Navigation Bar | Modal Drawer + Navigation Bar |249| Medium (600-839dp) | Navigation Rail | Modal Drawer + Navigation Rail |250| Expanded (840dp+) | Navigation Rail | Permanent Drawer |251252---253254## 3. Layout & Responsive [HIGH]255256### 3.1 Window Size Classes257258Use window size classes for adaptive layouts, not raw pixel breakpoints.259260```kotlin261// Compose: Window size classes262val windowSizeClass = calculateWindowSizeClass(this)263when (windowSizeClass.widthSizeClass) {264 WindowWidthSizeClass.Compact -> CompactLayout()265 WindowWidthSizeClass.Medium -> MediumLayout()266 WindowWidthSizeClass.Expanded -> ExpandedLayout()267}268```269270| Class | Width | Typical Device | Columns |271|-------|-------|----------------|---------|272| Compact | < 600dp | Phone portrait | 4 |273| Medium | 600-839dp | Tablet portrait, foldable | 8 |274| Expanded | 840dp+ | Tablet landscape, desktop | 12 |275276**Rules:**277- R3.1: Always use `WindowSizeClass` from `material3-window-size-class` for responsive layout decisions.278- R3.2: Never use fixed pixel breakpoints. Device categories are fluid.279- R3.3: Support all three width size classes. At minimum, compact and expanded.280281### 3.2 Material Grid282283Apply canonical Material grid margins and gutters.284285| Size Class | Margins | Gutters | Columns |286|------------|---------|---------|---------|287| Compact | 16dp | 8dp | 4 |288| Medium | 24dp | 16dp | 8 |289| Expanded | 24dp | 24dp | 12 |290291**Rules:**292- R3.4: Content should not span the full width on expanded screens. Use a max content width of ~840dp or list-detail layout.293- R3.5: Apply consistent horizontal margins matching the grid spec.294295### 3.3 Edge-to-Edge Display296297Android 15+ enforces edge-to-edge. All apps should draw behind system bars.298299```kotlin300// Compose: Edge-to-edge setup301class MainActivity : ComponentActivity() {302 override fun onCreate(savedInstanceState: Bundle?) {303 enableEdgeToEdge()304 super.onCreate(savedInstanceState)305 setContent {306 Scaffold(307 modifier = Modifier.fillMaxSize(),308 // Scaffold handles insets for top/bottom bars automatically309 ) { innerPadding ->310 Content(modifier = Modifier.padding(innerPadding))311 }312 }313 }314}315```316317**Rules:**318- R3.6: Call `enableEdgeToEdge()` before `setContent`. Draw behind both status bar and navigation bar.319- R3.7: Use `WindowInsets` to pad content away from system bars. `Scaffold` handles this for top bar and bottom bar content automatically.320- R3.8: Scrollable content should scroll behind transparent system bars with appropriate inset padding at the top and bottom of the list.321322### 3.4 Foldable Device Support323324```kotlin325// Compose: Detect fold posture326val foldingFeatures = WindowInfoTracker.getOrCreate(context)327 .windowLayoutInfo(context)328 .collectAsState(initial = WindowLayoutInfo(emptyList()))329```330331**Rules:**332- R3.9: Detect hinge/fold position and avoid placing critical content across the fold.333- R3.10: Use `ListDetailPaneScaffold` or `SupportingPaneScaffold` from Material3 adaptive library for foldable-aware layouts.334335---336337## 4. Typography [HIGH]338339### 4.1 Material Type Scale340341| Role | Default Size | Default Weight | Usage |342|------|-------------|----------------|-------|343| displayLarge | 57sp | 400 | Hero text, onboarding |344| displayMedium | 45sp | 400 | Large feature text |345| displaySmall | 36sp | 400 | Prominent display |346| headlineLarge | 32sp | 400 | Screen titles |347| headlineMedium | 28sp | 400 | Section headers |348| headlineSmall | 24sp | 400 | Card titles |349| titleLarge | 22sp | 400 | Top app bar title |350| titleMedium | 16sp | 500 | Tabs, navigation |351| titleSmall | 14sp | 500 | Subtitles |352| bodyLarge | 16sp | 400 | Primary body text |353| bodyMedium | 14sp | 400 | Secondary body text |354| bodySmall | 12sp | 400 | Captions |355| labelLarge | 14sp | 500 | Buttons, prominent labels |356| labelMedium | 12sp | 500 | Chips, smaller labels |357| labelSmall | 11sp | 500 | Timestamps, annotations |358359```kotlin360// Compose: Custom typography361val AppTypography = Typography(362 displayLarge = TextStyle(363 fontFamily = FontFamily(Font(R.font.brand_regular)),364 fontWeight = FontWeight.Normal,365 fontSize = 57.sp,366 lineHeight = 64.sp,367 letterSpacing = (-0.25).sp368 ),369 bodyLarge = TextStyle(370 fontFamily = FontFamily(Font(R.font.brand_regular)),371 fontWeight = FontWeight.Normal,372 fontSize = 16.sp,373 lineHeight = 24.sp,374 letterSpacing = 0.5.sp375 )376 // ... define all 15 roles377)378```379380**Rules:**381- R4.1: Always use `sp` units for text sizes to support user font scaling preferences.382- R4.2: Never set text below 12sp for body content. Labels may go to 11sp minimum.383- R4.3: Reference typography roles from `MaterialTheme.typography`, not hardcoded sizes.384- R4.4: Support dynamic type scaling. Test at 200% font scale. Ensure no text is clipped or overlapping.385- R4.5: Line height should be approximately 1.2-1.5x the font size for readability.386387---388389## 5. Components [HIGH]390391### 5.1 Floating Action Button (FAB)392393The FAB represents the single most important action on a screen.394395```kotlin396// Compose: FAB variants397// Standard FAB398FloatingActionButton(onClick = { /* action */ }) {399 Icon(Icons.Default.Add, contentDescription = "Create new item")400}401402// Extended FAB (with label - preferred for clarity)403ExtendedFloatingActionButton(404 onClick = { /* action */ },405 icon = { Icon(Icons.Default.Edit, contentDescription = null) },406 text = { Text("Compose") }407)408409// Large FAB410LargeFloatingActionButton(onClick = { /* action */ }) {411 Icon(Icons.Default.Add, contentDescription = "Create", modifier = Modifier.size(36.dp))412}413```414415**Rules:**416- R5.1: Use at most one FAB per screen. It represents the primary action.417- R5.2: Place the FAB at the bottom-end of the screen. On screens with a Navigation Bar, the FAB floats above it.418- R5.3: The FAB should use `primaryContainer` color by default. Use `tertiaryContainer` for secondary screens.419- R5.4: Prefer `ExtendedFloatingActionButton` with a label for clarity. Collapse to icon-only on scroll if needed.420421### 5.2 Top App Bar422423```kotlin424// Compose: Top app bar variants425// Small (default)426TopAppBar(427 title = { Text("Page Title") },428 navigationIcon = {429 IconButton(onClick = { /* navigate up */ }) {430 Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")431 }432 },433 actions = {434 IconButton(onClick = { /* search */ }) {435 Icon(Icons.Default.Search, contentDescription = "Search")436 }437 }438)439440// Medium — expands title area441MediumTopAppBar(442 title = { Text("Section Title") },443 scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()444)445446// Large — for prominent titles447LargeTopAppBar(448 title = { Text("Screen Title") },449 scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()450)451```452453**Rules:**454- R5.5: Use `TopAppBar` (small) for most screens. Use `MediumTopAppBar` or `LargeTopAppBar` for prominent section or screen titles.455- R5.6: Connect scroll behavior to the app bar so it collapses/expands with content scrolling.456- R5.7: Limit action icons to 2-3. Overflow additional actions into a more menu.457458### 5.3 Bottom Sheets459460```kotlin461// Compose: Modal bottom sheet462ModalBottomSheet(463 onDismissRequest = { showSheet = false },464 sheetState = rememberModalBottomSheetState()465) {466 Column(modifier = Modifier.padding(16.dp)) {467 Text("Sheet Title", style = MaterialTheme.typography.titleLarge)468 Spacer(modifier = Modifier.height(16.dp))469 // Sheet content470 }471}472```473474**Rules:**475- R5.8: Use modal bottom sheets for non-critical supplementary content. Use standard bottom sheets for persistent content.476- R5.9: Bottom sheets must have a visible drag handle for discoverability.477- R5.10: Sheet content must be scrollable if it can exceed the visible area.478479### 5.4 Dialogs480481```kotlin482// Compose: Alert dialog483AlertDialog(484 onDismissRequest = { showDialog = false },485 title = { Text("Discard draft?") },486 text = { Text("Your unsaved changes will be lost.") },487 confirmButton = {488 TextButton(onClick = { /* confirm */ }) { Text("Discard") }489 },490 dismissButton = {491 TextButton(onClick = { showDialog = false }) { Text("Cancel") }492 }493)494```495496**Rules:**497- R5.11: Dialogs interrupt the user. Use them only for critical decisions requiring immediate attention.498- R5.12: Confirm button uses a text button, not a filled button. The dismiss button is always on the left.499- R5.13: Dialog titles should be concise questions or statements. Body text provides context.500501### 5.5 Snackbar502503```kotlin504// Compose: Snackbar with action505val snackbarHostState = remember { SnackbarHostState() }506Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) {507 // trigger snackbar508 LaunchedEffect(key) {509 val result = snackbarHostState.showSnackbar(510 message = "Item archived",511 actionLabel = "Undo",512 duration = SnackbarDuration.Short513 )514 if (result == SnackbarResult.ActionPerformed) { /* undo */ }515 }516}517```518519**Rules:**520- R5.14: Use snackbars for brief, non-critical feedback. They auto-dismiss and should not contain critical information.521- R5.15: Snackbars appear at the bottom of the screen, above the Navigation Bar and below the FAB.522- R5.16: Include an action (e.g., "Undo") when the operation is reversible. Limit to one action.523524### 5.6 Chips525526```kotlin527// Filter Chip528FilterChip(529 selected = isSelected,530 onClick = { isSelected = !isSelected },531 label = { Text("Filter") },532 leadingIcon = if (isSelected) {533 { Icon(Icons.Default.Check, contentDescription = null, modifier = Modifier.size(18.dp)) }534 } else null535)536537// Assist Chip538AssistChip(539 onClick = { /* action */ },540 label = { Text("Add to calendar") },541 leadingIcon = { Icon(Icons.Default.CalendarToday, contentDescription = null) }542)543```544545**Rules:**546- R5.17: Use `FilterChip` for toggling filters, `AssistChip` for smart suggestions, `InputChip` for user-entered content (tags), `SuggestionChip` for dynamically generated suggestions.547- R5.18: Chips should be arranged in a horizontally scrollable row or a flow layout, not stacked vertically.548549### 5.7 Component Selection Guide550551| Need | Component |552|------|-----------|553| Primary screen action | FAB |554| Brief feedback | Snackbar |555| Critical decision | Dialog |556| Supplementary content | Bottom Sheet |557| Toggle filter | Filter Chip |558| User-entered tag | Input Chip |559| Smart suggestion | Assist Chip |560| Content group | Card |561| Vertical list of items | LazyColumn with ListItem |562| Segmented option (2-5) | SegmentedButton |563| Binary toggle | Switch |564| Selection from list | Radio buttons or exposed dropdown menu |565566---567568## 6. Accessibility [CRITICAL]569570### 6.1 TalkBack and Content Descriptions571572```kotlin573// Compose: Accessible components574Icon(575 Icons.Default.Favorite,576 contentDescription = "Add to favorites" // Descriptive, not "heart icon"577)578579// Decorative elements580Icon(581 Icons.Default.Star,582 contentDescription = null // null for purely decorative583)584585// Merge semantics for compound elements586Row(modifier = Modifier.semantics(mergeDescendants = true) {}) {587 Icon(Icons.Default.Event, contentDescription = null)588 Text("March 15, 2026")589}590591// Custom actions592Box(modifier = Modifier.semantics {593 customActions = listOf(594 CustomAccessibilityAction("Archive") { /* archive */ true },595 CustomAccessibilityAction("Delete") { /* delete */ true }596 )597})598```599600**Rules:**601- R6.1: Every interactive element must have a `contentDescription` (or `null` if purely decorative).602- R6.2: Content descriptions must describe the action or meaning, not the visual appearance. Say "Add to favorites" not "Heart icon."603- R6.3: Use `mergeDescendants = true` to group related elements into a single TalkBack focus unit (e.g., a list item with icon + text + subtitle).604- R6.4: Provide `customActions` for swipe-to-dismiss or long-press actions so TalkBack users can access them.605606### 6.2 Touch Targets607608```kotlin609// Compose: Ensure minimum touch target610IconButton(onClick = { /* action */ }) {611 // IconButton already provides 48dp minimum touch target612 Icon(Icons.Default.Close, contentDescription = "Close")613}614615// Manual minimum touch target616Box(617 modifier = Modifier618 .sizeIn(minWidth = 48.dp, minHeight = 48.dp)619 .clickable { /* action */ },620 contentAlignment = Alignment.Center621) {622 Icon(Icons.Default.Info, contentDescription = "Info", modifier = Modifier.size(24.dp))623}624```625626**Rules:**627- R6.5: All interactive elements must have a minimum touch target of 48x48dp. Material 3 components handle this by default.628- R6.6: Do not reduce touch targets to save space. Use padding to increase the touchable area if the visual element is smaller.629630### 6.3 Color Contrast and Visual631632**Rules:**633- R6.7: Text contrast ratio must be at least 4.5:1 for normal text and 3:1 for large text (18sp+ or 14sp+ bold) against its background.634- R6.8: Never use color as the only means of conveying information. Pair with icons, text, or patterns.635- R6.9: Support "bold text" and "high contrast" accessibility settings.636637### 6.4 Focus and Traversal638639```kotlin640// Compose: Custom focus order641Column {642 var focusRequester = remember { FocusRequester() }643 TextField(644 modifier = Modifier.focusRequester(focusRequester),645 value = text,646 onValueChange = { text = it }647 )648 LaunchedEffect(Unit) {649 focusRequester.requestFocus() // Auto-focus on screen load650 }651}652```653654**Rules:**655- R6.10: Focus order must follow a logical reading sequence (top-to-bottom, start-to-end). Avoid custom `focusOrder` unless the default is incorrect.656- R6.11: After navigation or dialog dismissal, move focus to the most logical target element.657- R6.12: All screens must be fully operable using TalkBack, Switch Access, and external keyboard.658659---660661## 7. Gestures & Input [MEDIUM]662663### 7.1 System Gestures664665**Rules:**666- R7.1: Never place interactive elements within the system gesture inset zones (bottom 20dp, left/right 24dp edges) as they conflict with system navigation gestures.667- R7.2: Use `WindowInsets.systemGestures` to detect and avoid gesture conflict zones.668669### 7.2 Common Gesture Patterns670671```kotlin672// Compose: Pull to refresh673PullToRefreshBox(674 isRefreshing = isRefreshing,675 onRefresh = { viewModel.refresh() }676) {677 LazyColumn { /* content */ }678}679680// Compose: Swipe to dismiss681SwipeToDismissBox(682 state = rememberSwipeToDismissBoxState(),683 backgroundContent = {684 Box(685 modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.error),686 contentAlignment = Alignment.CenterEnd687 ) {688 Icon(Icons.Default.Delete, contentDescription = "Delete",689 tint = MaterialTheme.colorScheme.onError)690 }691 }692) {693 ListItem(headlineContent = { Text("Swipeable item") })694}695```696697**Rules:**698- R7.3: All swipe-to-dismiss actions must be undoable (show snackbar with undo) or require confirmation.699- R7.4: Provide alternative non-gesture ways to trigger all gesture-based actions (for accessibility).700- R7.5: Apply Material ripple effect on all tappable elements. Compose `clickable` modifier includes ripple by default.701702### 7.3 Long Press703704**Rules:**705- R7.6: Use long press for contextual menus and multi-select mode. Never use it as the only way to access a feature.706- R7.7: Provide haptic feedback on long press via `HapticFeedbackType.LongPress`.707708---709710## 8. Notifications [MEDIUM]711712### 8.1 Notification Channels713714```kotlin715// Create notification channel (required for Android 8+)716val channel = NotificationChannel(717 "messages",718 "Messages",719 NotificationManager.IMPORTANCE_HIGH720).apply {721 description = "New message notifications"722 enableLights(true)723 lightColor = Color.BLUE724}725notificationManager.createNotificationChannel(channel)726```727728| Importance | Behavior | Use For |729|-----------|----------|---------|730| IMPORTANCE_HIGH | Sound + heads-up | Messages, calls |731| IMPORTANCE_DEFAULT | Sound | Social updates, emails |732| IMPORTANCE_LOW | No sound | Recommendations |733| IMPORTANCE_MIN | Silent, no status bar | Weather, ongoing |734735**Rules:**736- R8.1: Create separate notification channels for each distinct notification type. Users can configure each independently.737- R8.2: Choose importance levels conservatively. Overusing `IMPORTANCE_HIGH` leads users to disable notifications entirely.738- R8.3: All notifications must have a tap action (PendingIntent) that navigates to relevant content.739- R8.4: Include a `contentDescription` in notification icons for accessibility.740741### 8.2 Notification Design742743**Rules:**744- R8.5: Use `MessagingStyle` for conversations. Include sender name and avatar.745- R8.6: Add direct reply actions to messaging notifications.746- R8.7: Provide a "Mark as read" action on message notifications.747- R8.8: Use expandable notifications (`BigTextStyle`, `BigPictureStyle`, `InboxStyle`) for rich content.748- R8.9: Foreground service notifications must accurately describe the ongoing operation and provide a stop action where appropriate.749750---751752## 9. Permissions & Privacy [HIGH]753754### 9.1 Runtime Permissions755756```kotlin757// Compose: Permission request758val permissionState = rememberPermissionState(Manifest.permission.CAMERA)759760if (permissionState.status.isGranted) {761 CameraPreview()762} else {763 Column {764 Text("Camera access is needed to scan QR codes.")765 Button(onClick = { permissionState.launchPermissionRequest() }) {766 Text("Grant Camera Access")767 }768 }769}770```771772**Rules:**773- R9.1: Request permissions in context, at the moment they are needed, not at app launch.774- R9.2: Always explain why the permission is needed before requesting it (rationale screen).775- R9.3: Gracefully handle permission denial. Provide degraded functionality rather than blocking the user.776- R9.4: Never request permissions you do not actively use. Google Play will reject apps with unnecessary permissions.777778### 9.2 Privacy-Preserving APIs779780```kotlin781// Photo picker: no permission needed782val pickMedia = rememberLauncherForActivityResult(783 ActivityResultContracts.PickVisualMedia()784) { uri ->785 uri?.let { /* handle selected media */ }786}787pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))788```789790**Rules:**791- R9.5: Use the Photo Picker (Android 13+) instead of requesting `READ_MEDIA_IMAGES`. No permission needed.792- R9.6: Use `ACCESS_COARSE_LOCATION` (approximate) unless precise location is essential for functionality.793- R9.7: Prefer one-time permissions for camera and microphone in non-recording contexts.794- R9.8: Display a privacy indicator when camera or microphone is actively in use.795796---797798## 10. System Integration [MEDIUM]799800### 10.1 Widgets801802```kotlin803// Compose Glance API widget804class TaskWidget : GlanceAppWidget() {805 override suspend fun provideGlance(context: Context, id: GlanceId) {806 provideContent {807 GlanceTheme {808 Column(809 modifier = GlanceModifier810 .fillMaxSize()811 .background(GlanceTheme.colors.widgetBackground)812 .padding(16.dp)813 ) {814 Text(815 text = "Tasks",816 style = TextStyle(fontWeight = FontWeight.Bold,817 color = GlanceTheme.colors.onSurface)818 )819 // Widget content820 }821 }822 }823 }824}825```826827**Rules:**828- R10.1: Use Glance API for new widgets. Support dynamic color via `GlanceTheme`.829- R10.2: Widgets must have a default configuration and work immediately after placement.830- R10.3: Provide multiple widget sizes (small, medium, large) where practical.831- R10.4: Use rounded corners matching the system widget shape (`system_app_widget_background_radius`).832833### 10.2 App Shortcuts834835```xml836<!-- shortcuts.xml -->837<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">838 <shortcut839 android:shortcutId="compose"840 android:enabled="true"841 android:shortcutShortLabel="@string/compose_short"842 android:shortcutLongLabel="@string/compose_long"843 android:icon="@drawable/ic_shortcut_compose">844 <intent845 android:action="android.intent.action.VIEW"846 android:targetPackage="com.example.app"847 android:targetClass="com.example.app.ComposeActivity" />848 </shortcut>849</shortcuts>850```851852**Rules:**853- R10.5: Provide 2-4 static shortcuts for common actions. Support dynamic shortcuts for recent content.854- R10.6: Shortcut icons should be simple, recognizable silhouettes on a circular background.855- R10.7: Test shortcuts with long-press on the app icon and in the Settings > Apps shortcut list.856857### 10.3 Deep Links and Share858859**Rules:**860- R10.8: Support Android App Links (verified deep links) for all public content URLs.861- R10.9: Implement the share sheet with `ShareCompat` or `Intent.createChooser`. Provide rich previews with title, description, and thumbnail.862- R10.10: Handle incoming share intents with appropriate content type filtering.863864---865866## Design Evaluation Checklist867868Use this checklist to evaluate Android UI implementations:869870### Theme & Color871- [ ] Dynamic color enabled with static fallback872- [ ] All colors reference Material theme roles (no hardcoded hex)873- [ ] Light and dark themes both supported874- [ ] On-colors match their background color roles875- [ ] Custom colors generated from seed via Material Theme Builder876877### Navigation878- [ ] Correct navigation component for screen size and destination count879- [ ] Navigation bar labels always visible880- [ ] Predictive back gesture opted in and handled881- [ ] Up vs Back behavior correct882883### Layout884- [ ] All three window size classes supported885- [ ] Edge-to-edge with proper inset handling886- [ ] Content does not span full width on large screens887- [ ] Foldable hinge area respected888889### Typography890- [ ] All text uses sp units891- [ ] All text references MaterialTheme.typography roles892- [ ] Tested at 200% font scale with no clipping893- [ ] Minimum 12sp body, 11sp labels894895### Components896- [ ] At most one FAB per screen897- [ ] Top app bar connected to scroll behavior898- [ ] Snackbars used for non-critical feedback only899- [ ] Dialogs reserved for critical interruptions900901### Accessibility902- [ ] All interactive elements have contentDescription903- [ ] All touch targets >= 48dp904- [ ] Color contrast >= 4.5:1 for text905- [ ] No information conveyed by color alone906- [ ] Full TalkBack traversal tested907- [ ] Switch Access and keyboard navigation work908909### Gestures910- [ ] No interactive elements in system gesture zones911- [ ] All gesture actions have non-gesture alternatives912- [ ] Swipe-to-dismiss is undoable913914### Notifications915- [ ] Separate channels for each notification type916- [ ] Appropriate importance levels917- [ ] Tap action navigates to relevant content918919### Permissions920- [ ] Permissions requested in context, not at launch921- [ ] Rationale shown before permission request922- [ ] Graceful degradation on denial923- [ ] Photo Picker used instead of media permission924925### System Integration926- [ ] Widgets use Glance API with dynamic color927- [ ] App shortcuts provided for common actions928- [ ] Deep links handled for public content929930---931932## Anti-Patterns933934| Anti-Pattern | Why It Is Wrong | Correct Approach |935|-------------|----------------|------------------|936| Hardcoded color hex values | Breaks dynamic color and dark theme | Use `MaterialTheme.colorScheme` roles |937| Using `dp` for text size | Ignores user font scaling | Use `sp` units |938| Custom bottom navigation bar | Inconsistent with platform | Use Material `NavigationBar` |939| Navigation bar without labels | Violates Material guidelines | Always show labels |940| Dialog for non-critical info | Interrupts user unnecessarily | Use Snackbar or Bottom Sheet |941| FAB for secondary actions | Dilutes primary action prominence | One FAB for the primary action only |942| `onBackPressed()` override | Deprecated; breaks predictive back | Use `OnBackInvokedCallback` |943| Touch targets < 48dp | Accessibility violation | Ensure minimum 48x48dp |944| Permission request at launch | Users deny without context | Request in context with rationale |945| Pure black (#000000) dark theme | Eye strain; not Material 3 | Use Material surface color roles |946| Icon-only navigation bar | Users cannot identify destinations | Always include text labels |947| Full-width content on tablets | Wastes space; poor readability | Max width or list-detail layout |948| `READ_EXTERNAL_STORAGE` for photos | Unnecessary since Android 13 | Use Photo Picker API |949| Blocking UI on permission denial | Punishes the user | Graceful degradation |950| Manual color palette selection | Inconsistent tonal relationships | Use Material Theme Builder |951
Full transparency — inspect the skill content before installing.