К основному содержанию
скилл CX Codex

android-design-guidelines — скилл для Claude Code: правила Material 3

Скилл для ИИ-агента, который держит Android-интерфейс в рамках Material Design 3: динамический цвет, роли цветов, навигация.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add ehmo/platform-design-skills --skill android-design-guidelines -a claude-code
npx skills add ehmo/platform-design-skills --skill android -a claude-code
Открыть на GitHub

Что делает android-design-guidelines

Скилл описывает правила Material Design 3 для Android-приложений. Внутри — разделы про Material You и темы, навигацию, а также код на Kotlin для Jetpack Compose и примеры XML для тем. Агент читает эти правила, когда вы просите сверстать экран, настроить тему или разобраться с динамическим цветом.

Динамический цвет из обоев пользователя здесь основная стратегия, но для устройств ниже Android 12 нужна статическая палитра. Цвета берутся по ролям: primary, onPrimary, surface, errorContainer и так далее. Хардкод hex-значений в компонентах считается антипаттерном, поэтому проверьте, не осталось ли у вас в коде прямых Color(0xFF...).

Навигация расписана по размерам экрана: нижняя панель для телефонов, рейл для планшетов и складных, ящик для сложных иерархий. Метки на нижней панели обязательны, иконки только для выбранного состояния залитые. Текст обрезан на разделе про Navigation Drawer, так что полного описания всех сценариев навигации в нём нет.

Какие задачи решает

  • настроить тему Compose с динамическим цветом и фолбэком
  • разложить цвета по ролям вместо хардкода hex
  • выбрать навигацию под размер экрана
  • поддержать светлую и тёмную тему без чистого чёрного
  • проверить, что цвета сочетаются с разными обоями

Примеры запросов агенту

  • Собери экран настроек на Compose с цветами из MaterialTheme, без хардкода
  • Проверь тему приложения: есть ли фолбэк для устройств ниже Android 12?
  • Добавь нижнюю навигацию на 4 раздела с метками и залитыми иконками

Как установить android-design-guidelines

1Через Skills CLI (Vercel)

npx skills add ehmo/platform-design-skills --skill android-design-guidelines -a claude-code
npx skills add ehmo/platform-design-skills --skill android -a claude-code

2Вручную: положить файлы

~/.claude/skills/android/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/android/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Вопросы о android-design-guidelines

Как установить android-design-guidelines в Claude?

Через skills CLI: npx skills add ehmo/platform-design-skills --skill android-design-guidelines -a claude-code. После установки файл скилла лежит в ~/.claude/skills/android/SKILL.md.

Что делает android-design-guidelines?

Задаёт агенту правила Material Design 3 для Android: динамический цвет, роли цветов, светлая и тёмная темы, паттерны навигации. Работает при вёрстке на Jetpack Compose и в XML-разметке.

Какие платформы поддерживаются?

Скилл ставится в Claude, Codex, Cursor, GitHub Copilot, Gemini CLI и OpenCode. Для каждой платформы своя команда установки.

Исходный текст

SKILL.md · MIT
---
name: android-design-guidelines
description: 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.
license: MIT
metadata:
  author: platform-design-skills
  version: "1.0.0"
---

# 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.

```kotlin
// Compose: Dynamic color theme
@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
    )
}
```

```xml
<!-- XML: Dynamic color in themes.xml -->
<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` |

```kotlin
// Correct: semantic color roles
Text(
    text = "Error message",
    color = MaterialTheme.colorScheme.error
)
Surface(color = MaterialTheme.colorScheme.errorContainer) {
    Text(text = "Error detail", color = MaterialTheme.colorScheme.onErrorContainer)
}

// WRONG: hardcoded colors
Text(text = "Error", color = Color(0xFFB00020)) // Anti-pattern
```

**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.

```kotlin
// Compose: Detect system theme
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.

```kotlin
// Custom color scheme with brand seed
private val BrandLightColorScheme = lightColorScheme(
    primary = Color(0xFF1B6D2F),
    onPrimary = Color(0xFFFFFFFF),
    primaryContainer = Color(0xFFA4F6A8),
    onPrimaryContainer = Color(0xFF002107),
    // ... generate full palette from seed
)
```

**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.

```kotlin
// Compose: Navigation Bar
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).

```kotlin
// Compose: Navigation Rail for larger screens
NavigationRail(
    header = {
        FloatingActionButton(
            onClick = { /* primary action */ },
            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.

```kotlin
// Compose: Permanent Navigation Drawer for large screens
PermanentNavigationDrawer(
    drawerContent = {
        PermanentDrawerSheet {
            Text("App Name", modifier = Modifier.padding(16.dp),
                 style = MaterialTheme.typography.titleMedium)
            HorizontalDivider()
            items.forEach { item ->
                NavigationDrawerItem(
                    label = { Text(item.label) },
                    selected = item == selectedItem,
                    onClick = { selectedItem = item },
                    icon = { Icon(item.icon, contentDescription = null) }
                )
            }
        }
    }
) {
    Scaffold { /* page content */ }
}
```

**Rules:**
- R2.8: Use modal drawer on compact screens, permanent drawer on expanded screens.
- R2.9: Group drawer items into sections with dividers and section headers.

### 2.4 Predictive Back Gesture

Android 13+ supports predictive back with an animation preview.

```kotlin
// Compose: Predictive back with BackHandler (androidx.activity.compose)
BackHandler(enabled = true) {
    // Called when back is confirmed; navigate back in your nav controller
    navController.popBackStack()
}
```

```kotlin
// Compose: Predictive back progress animation using predictiveBackHandler modifier
// (androidx.activity:activity-compose 1.8+)
Modifier.predictiveBackHandler(enabled = true) { progress ->
    // progress is a Flow<BackEventCompat> with x, y, swipeEdge, progress (0.0–1.0)
    progress.collect { backEvent ->
        animationState = backEvent.progress
    }
}
```

```xml
<!-- AndroidManifest.xml: opt in to predictive back -->
<application android:enableOnBackInvokedCallback="true">
```

**Rules:**
- R2.10: Opt in to predictive back in the manifest. In **Compose** apps, use `BackHandler` (from `androidx.activity.compose`) to intercept back events. In **View-based** apps, implement `OnBackInvokedCallback` (API 33+) or `OnBackPressedCallback` (AndroidX) instead of overriding `onBackPressed()`.
- 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.
- R2.12: Never intercept system back to show "are you sure?" dialogs unless there is unsaved user input.
- R2.13: Do not suppress the system-provided back preview animation. If you implement custom enter/exit transitions, interpolate them using `BackEventCompat.progress` (0.0–1.0) and respect `BackEventCompat.swipeEdge` (`EDGE_LEFT`/`EDGE_RIGHT`) so the exiting screen scales down and shifts toward the initiating edge, matching the system animation.
- R2.14: Prefer recognition over recall. Keep destinations labeled, selected state visible, and back-stack context preserved so users do not reconstruct where they are after every navigation step.

```kotlin
// Compose: drive a custom animation from predictive back progress
Modifier.predictiveBackHandler(enabled = true) { progress ->
    progress.collect { backEvent ->
        // backEvent.progress: 0.0 (gesture start) → 1.0 (committed)
        // backEvent.swipeEdge: BackEventCompat.EDGE_LEFT or EDGE_RIGHT
        exitScale = 1f - (backEvent.progress * 0.1f)
        exitOffsetX = if (backEvent.swipeEdge == BackEventCompat.EDGE_LEFT) -backEvent.progress * 32.dp.toPx() else backEvent.progress * 32.dp.toPx()
    }
}
```

### 2.5 Navigation Component Selection

| Screen Size | 3-5 Destinations | 5+ Destinations |
|-------------|-------------------|-----------------|
| Compact (< 600dp) | Navigation Bar | Modal Drawer + Navigation Bar |
| Medium (600-839dp) | Navigation Rail | Modal Drawer + Navigation Rail |
| Expanded (840dp+) | Navigation Rail | Permanent Drawer |

---

## 3. Layout & Responsive [HIGH]

### 3.1 Window Size Classes

Use window size classes for adaptive layouts, not raw pixel breakpoints.

```kotlin
// Compose: Window size classes
val windowSizeClass = calculateWindowSizeClass(this)
when (windowSizeClass.widthSizeClass) {
    WindowWidthSizeClass.Compact -> CompactLayout()
    WindowWidthSizeClass.Medium -> MediumLayout()
    WindowWidthSizeClass.Expanded -> ExpandedLayout()
}
```

| Class | Width | Typical Device | Columns |
|-------|-------|----------------|---------|
| Compact | < 600dp | Phone portrait | 4 |
| Medium | 600-839dp | Tablet portrait, foldable | 8 |
| Expanded | 840dp+ | Tablet landscape, desktop | 12 |

**Rules:**
- R3.1: Always use `WindowSizeClass` from `material3-window-size-class` for responsive layout decisions.
- R3.2: Never use fixed pixel breakpoints. Device categories are fluid.
- R3.3: Support all three width size classes. At minimum, compact and expanded.

### 3.2 Material Grid

Apply canonical Material grid margins and gutters.

| Size Class | Margins | Gutters | Columns |
|------------|---------|---------|---------|
| Compact | 16dp | 8dp | 4 |
| Medium | 24dp | 16dp | 8 |
| Expanded | 24dp | 24dp | 12 |

**Rules:**
- R3.4: Content should not span the full width on expanded screens. Use a max content width of ~840dp or list-detail layout.
- R3.5: Apply consistent horizontal margins matching the grid spec.

### 3.3 Edge-to-Edge Display

Android 15+ enforces edge-to-edge. All apps should draw behind system bars.

```kotlin
// Compose: Edge-to-edge setup
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge()
        super.onCreate(savedInstanceState)
        setContent {
            Scaffold(
                modifier = Modifier.fillMaxSize(),
                // Scaffold handles insets for top/bottom bars automatically
            ) { innerPadding ->
                Content(modifier = Modifier.padding(innerPadding))
            }
        }
    }
}
```

**Rules:**
- R3.6: Call `enableEdgeToEdge()` before `setContent`. Draw behind both status bar and navigation bar.
- R3.7: Use `WindowInsets` to pad content away from system bars. `Scaffold` handles this for top bar and bottom bar content automatically.
- R3.8: Scrollable content should scroll behind transparent system bars with appropriate inset padding at the top and bottom of the list.

### 3.4 Foldable Device Support

```kotlin
// Compose: Detect fold posture
val foldingFeatures = WindowInfoTracker.getOrCreate(context)
    .windowLayoutInfo(context)
    .collectAsState(initial = WindowLayoutInfo(emptyList()))
```

**Rules:**
- R3.9: Detect hinge/fold position and avoid placing critical content across the fold.
- R3.10: Use `ListDetailPaneScaffold` or `SupportingPaneScaffold` from Material3 adaptive library for foldable-aware layouts.

---

## 4. Typography [HIGH]

### 4.1 Material Type Scale

| Role | Default Size | Default Weight | Usage |
|------|-------------|----------------|-------|
| displayLarge | 57sp | 400 | Hero text, onboarding |
| displayMedium | 45sp | 400 | Large feature text |
| displaySmall | 36sp | 400 | Prominent display |
| headlineLarge | 32sp | 400 | Screen titles |
| headlineMedium | 28sp | 400 | Section headers |
| headlineSmall | 24sp | 400 | Card titles |
| titleLarge | 22sp | 400 | Top app bar title |
| titleMedium | 16sp | 500 | Tabs, navigation |
| titleSmall | 14sp | 500 | Subtitles |
| bodyLarge | 16sp | 400 | Primary body text |
| bodyMedium | 14sp | 400 | Secondary body text |
| bodySmall | 12sp | 400 | Captions |
| labelLarge | 14sp | 500 | Buttons, prominent labels |
| labelMedium | 12sp | 500 | Chips, smaller labels |
| labelSmall | 11sp | 500 | Timestamps, annotations |

```kotlin
// Compose: Custom typography
val AppTypography = Typography(
    displayLarge = TextStyle(
        fontFamily = FontFamily(Font(R.font.brand_regular)),
        fontWeight = FontWeight.Normal,
        fontSize = 57.sp,
        lineHeight = 64.sp,
        letterSpacing = (-0.25).sp
    ),
    bodyLarge = TextStyle(
        fontFamily = FontFamily(Font(R.font.brand_regular)),
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.5.sp
    )
    // ... define all 15 roles
)
```

**Rules:**
- R4.1: Always use `sp` units for text sizes to support user font scaling preferences.
- R4.2: Never set text below 12sp for body content. Labels may go to 11sp minimum.
- R4.3: Reference typography roles from `MaterialTheme.typography`, not hardcoded sizes.
- R4.4: Support dynamic type scaling. Test at 200% font scale. Ensure no text is clipped or overlapping.
- R4.5: Line height should be approximately 1.2-1.5x the font size for readability.

---

## 5. Components [HIGH]

### 5.1 Floating Action Button (FAB)

The FAB represents the single most important action on a screen.

```kotlin
// Compose: FAB variants
// Standard FAB
FloatingActionButton(onClick = { /* action */ }) {
    Icon(Icons.Default.Add, contentDescription = "Create new item")
}

// Extended FAB (with label - preferred for clarity)
ExtendedFloatingActionButton(
    onClick = { /* action */ },
    icon = { Icon(Icons.Default.Edit, contentDescription = null) },
    text = { Text("Compose") }
)

// Large FAB
LargeFloatingActionButton(onClick = { /* action */ }) {
    Icon(Icons.Default.Add, contentDescription = "Create", modifier = Modifier.size(36.dp))
}
```

**Rules:**
- R5.1: Use at most one FAB per screen. It represents the primary action.
- R5.2: Place the FAB at the bottom-end of the screen. On screens with a Navigation Bar, the FAB floats above it.
- R5.3: The FAB should use `primaryContainer` color by default. Use `tertiaryContainer` for secondary screens.
- R5.4: Prefer `ExtendedFloatingActionButton` with a label for clarity. Collapse to icon-only on scroll if needed.

### 5.2 Top App Bar

```kotlin
// Compose: Top app bar variants
// Small (default)
TopAppBar(
    title = { Text("Page Title") },
    navigationIcon = {
        IconButton(onClick = { /* navigate up */ }) {
            Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
        }
    },
    actions = {
        IconButton(onClick = { /* search */ }) {
            Icon(Icons.Default.Search, contentDescription = "Search")
        }
    }
)

// Medium — expands title area
MediumTopAppBar(
    title = { Text("Section Title") },
    scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
)

// Large — for prominent titles
LargeTopAppBar(
    title = { Text("Screen Title") },
    scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
)
```

**Rules:**
- R5.5: Use `TopAppBar` (small) for most screens. Use `MediumTopAppBar` or `LargeTopAppBar` for prominent section or screen titles.
- R5.6: Connect scroll behavior to the app bar so it collapses/expands with content scrolling.
- R5.7: Limit action icons to 2-3. Overflow additional actions into a more menu.

### 5.3 Bottom Sheets

```kotlin
// Compose: Modal bottom sheet
ModalBottomSheet(
    onDismissRequest = { showSheet = false },
    sheetState = rememberModalBottomSheetState()
) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text("Sheet Title", style = MaterialTheme.typography.titleLarge)
        Spacer(modifier = Modifier.height(16.dp))
        // Sheet content
    }
}
```

**Rules:**
- R5.8: Use modal bottom sheets for non-critical supplementary content. Use standard bottom sheets for persistent content.
- R5.9: Bottom sheets must have a visible drag handle for discoverability.
- R5.10: Sheet content must be scrollable if it can exceed the visible area.

### 5.4 Dialogs

```kotlin
// Compose: Alert dialog
AlertDialog(
    onDismissRequest = { showDialog = false },
    title = { Text("Discard draft?") },
    text = { Text("Your unsaved changes will be lost.") },
    confirmButton = {
        TextButton(onClick = { /* confirm */ }) { Text("Discard") }
    },
    dismissButton = {
        TextButton(onClick = { showDialog = false }) { Text("Cancel") }
    }
)
```

**Rules:**
- R5.11: Dialogs interrupt the user. Use them only for critical decisions requiring immediate attention.
- R5.12: Confirm button uses a text button, not a filled button. The dismiss button is always on the left.
- R5.13: Dialog titles should be concise questions or statements. Body text provides context.

### 5.5 Snackbar

```kotlin
// Compose: Snackbar with action
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) {
    // trigger snackbar
    LaunchedEffect(key) {
        val result = snackbarHostState.showSnackbar(
            message = "Item archived",
            actionLabel = "Undo",
            duration = SnackbarDuration.Short
        )
        if (result == SnackbarResult.ActionPerformed) { /* undo */ }
    }
}
```

**Rules:**
- R5.14: Use snackbars for brief, non-critical feedback. They auto-dismiss and should not contain critical information.
- R5.15: Snackbars appear at the bottom of the screen, above the Navigation Bar and below the FAB.
- R5.16: Include an action (e.g., "Undo") when the operation is reversible. Limit to one action.

### 5.6 Chips

```kotlin
// Filter Chip
FilterChip(
    selected = isSelected,
    onClick = { isSelected = !isSelected },
    label = { Text("Filter") },
    leadingIcon = if (isSelected) {
        { Icon(Icons.Default.Check, contentDescription = null, modifier = Modifier.size(18.dp)) }
    } else null
)

// Assist Chip
AssistChip(
    onClick = { /* action */ },
    label = { Text("Add to calendar") },
    leadingIcon = { Icon(Icons.Default.CalendarToday, contentDescription = null) }
)
```

**Rules:**
- R5.17: Use `FilterChip` for toggling filters, `AssistChip` for smart suggestions, `InputChip` for user-entered content (tags), `SuggestionChip` for dynamically generated suggestions.
- R5.18: Chips should be arranged in a horizontally scrollable row or a flow layout, not stacked vertically.
- R5.19: Expose waiting states immediately. If an action cannot finish right away, acknowledge it with inline state change, progress, or another visible response rather than leaving the UI static.

### 5.7 Component Selection Guide

| Need | Component |
|------|-----------|
| Primary screen action | FAB |
| Brief feedback | Snackbar |
| Critical decision | Dialog |
| Supplementary content | Bottom Sheet |
| Toggle filter | Filter Chip |
| User-entered tag | Input Chip |
| Smart suggestion | Assist Chip |
| Content group | Card |
| Vertical list of items | LazyColumn with ListItem |
| Segmented option (2-5) | SegmentedButton |
| Binary toggle | Switch |
| Selection from list | Radio buttons or exposed dropdown menu |

---

## 6. Accessibility [CRITICAL]

### 6.1 TalkBack and Content Descriptions

```kotlin
// Compose: Accessible components
Icon(
    Icons.Default.Favorite,
    contentDescription = "Add to favorites" // Descriptive, not "heart icon"
)

// Decorative elements
Icon(
    Icons.Default.Star,
    contentDescription = null // null for purely decorative
)

// Merge semantics for compound elements
Row(modifier = Modifier.semantics(mergeDescendants = true) {}) {
    Icon(Icons.Default.Event, contentDescription = null)
    Text("March 15, 2026")
}

// Custom actions
Box(modifier = Modifier.semantics {
    customActions = listOf(
        CustomAccessibilityAction("Archive") { /* archive */ true },
        CustomAccessibilityAction("Delete") { /* delete */ true }
    )
})
```

**Rules:**
- R6.1: Every interactive element must have a `contentDescription` (or `null` if purely decorative).
- R6.2: Content descriptions must describe the action or meaning, not the visual appearance. Say "Add to favorites" not "Heart icon."
- R6.3: Use `mergeDescendants = true` to group related elements into a single TalkBack focus unit (e.g., a list item with icon + text + subtitle).
- R6.4: Provide `customActions` for swipe-to-dismiss or long-press actions so TalkBack users can access them.

### 6.2 Touch Targets

```kotlin
// Compose: Ensure minimum touch target
IconButton(onClick = { /* action */ }) {
    // IconButton already provides 48dp minimum touch target
    Icon(Icons.Default.Close, contentDescription = "Close")
}

// Manual minimum touch target
Box(
    modifier = Modifier
        .sizeIn(minWidth = 48.dp, minHeight = 48.dp)
        .clickable { /* action */ },
    contentAlignment = Alignment.Center
) {
    Icon(Icons.Default.Info, contentDescription = "Info", modifier = Modifier.size(24.dp))
}
```

**Rules:**
- R6.5: All interactive elements must have a minimum touch target of 48x48dp. Material 3 components handle this by default.
- R6.6: Do not reduce touch targets to save space. Use padding to increase the touchable area if the visual element is smaller.

### 6.3 Color Contrast and Visual

**Rules:**
- 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.
- R6.8: Never use color as the only means of conveying information. Pair with icons, text, or patterns.
- R6.9: Support bold text and high contrast accessibility settings. Use `Configuration.fontWeightAdjustment` (API 31+) to detect the user's bold text preference and scale custom font weights accordingly. Use `AccessibilityManager.isHighTextContrastEnabled()` to detect high contrast mode and substitute higher-contrast color values. Material 3 components handle both automatically; custom text rendering and color usage must opt in explicitly.

```kotlin
// Detect bold text preference (API 31+)
val fontWeightAdjustment = resources.configuration.fontWeightAdjustment
val isBoldText = fontWeightAdjustment >= 700 // equivalent to FontWeight.Bold.weight

// Detect high contrast mode
val am = getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
val isHighContrast = am.isHighTextContrastEnabled

// Compose: use MaterialTheme.typography which respects fontWeightAdjustment automatically
Text(
    text = "Label",
    style = MaterialTheme.typography.bodyLarge // Adapts to fontWeightAdjustment
)

// For custom colors: provide high-contrast alternative
val labelColor = if (isHighContrast) {
    MaterialTheme.colorScheme.onSurface  // Strong contrast
} else {
    MaterialTheme.colorScheme.onSurfaceVariant  // Normal contrast
}
```

### 6.4 Focus and Traversal

```kotlin
// Compose: Custom focus order
Column {
    var focusRequester = remember { FocusRequester() }
    TextField(
        modifier = Modifier.focusRequester(focusRequester),
        value = text,
        onValueChange = { text = it }
    )
    LaunchedEffect(Unit) {
        focusRequester.requestFocus() // Auto-focus on screen load
    }
}
```

**Rules:**
- R6.10: Focus order must follow a logical reading sequence (top-to-bottom, start-to-end). Avoid custom `focusOrder` unless the default is incorrect.
- R6.11: After navigation or dialog dismissal, move focus to the most logical target element.
- R6.12: All screens must be fully operable using TalkBack, Switch Access, and external keyboard.

### 6.5 Custom Canvas Views

Custom `View` subclasses that draw content on a Canvas (charts, custom pickers, drawing surfaces) are invisible to TalkBack by default because they have no child views. Use `ExploreByTouchHelper` from `androidx.customview.widget` to define a virtual accessibility tree.

- R6.13: Custom canvas-drawn views must use `ExploreByTouchHelper` to expose a virtual accessibility tree to TalkBack. Override `getVirtualViewAt()` to map touch coordinates to virtual view IDs, and `onPopulateNodeForVirtualView()` to supply text, bounds, and actions for each virtual node.

```kotlin
import androidx.customview.widget.ExploreByTouchHelper

class PieChartView(context: Context) : View(context) {

    private val helper = object : ExploreByTouchHelper(this) {
        override fun getVirtualViewAt(x: Float, y: Float): Int {
            // Return virtual view ID for the slice at (x, y), or INVALID_ID
            return sliceIndexAt(x, y)
        }

        override fun getVisibleVirtualViews(virtualViewIds: MutableList<Int>) {
            slices.indices.forEach { virtualViewIds.add(it) }
        }

        override fun onPopulateNodeForVirtualView(
            virtualViewId: Int,
            node: AccessibilityNodeInfoCompat
        ) {
            val slice = slices[virtualViewId]
            node.text = "${slice.label}: ${slice.percentage}%"
            node.setBoundsInParent(slice.bounds)
            node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK)
        }

        override fun onPerformActionForVirtualView(
            virtualViewId: Int, action: Int, arguments: Bundle?
        ): Boolean {
            if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
                onSliceSelected(virtualViewId)
                return true
            }
            return false
        }
    }

    init {
        ViewCompat.setAccessibilityDelegate(this, helper)
    }

    override fun dispatchHoverEvent(event: MotionEvent) =
        helper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event)
}
```

---

## 7. Gestures & Input [MEDIUM]

### 7.1 System Gestures

**Rules:**
- 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.
- R7.2: Use `WindowInsets.systemGestures` to detect and avoid gesture conflict zones.

### 7.2 Common Gesture Patterns

```kotlin
// Compose: Pull to refresh
PullToRefreshBox(
    isRefreshing = isRefreshing,
    onRefresh = { viewModel.refresh() }
) {
    LazyColumn { /* content */ }
}

// Compose: Swipe to dismiss
SwipeToDismissBox(
    state = rememberSwipeToDismissBoxState(),
    backgroundContent = {
        Box(
            modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.error),
            contentAlignment = Alignment.CenterEnd
        ) {
            Icon(Icons.Default.Delete, contentDescription = "Delete",
                 tint = MaterialTheme.colorScheme.onError)
        }
    }
) {
    ListItem(headlineContent = { Text("Swipeable item") })
}
```

**Rules:**
- R7.3: All swipe-to-dismiss actions must be undoable (show snackbar with undo) or require confirmation.
- R7.4: Provide alternative non-gesture ways to trigger all gesture-based actions (for accessibility).
- R7.5: Apply Material ripple effect on all tappable elements. Compose `clickable` modifier includes ripple by default.

### 7.3 Long Press

**Rules:**
- R7.6: Use long press for contextual menus and multi-select mode. Never use it as the only way to access a feature.
- R7.7: Provide haptic feedback on long press via `HapticFeedbackType.LongPress`.

---

## 8. Notifications [MEDIUM]

### 8.1 Notification Channels

```kotlin
// Create notification channel (required for Android 8+)
val channel = NotificationChannel(
    "messages",
    "Messages",
    NotificationManager.IMPORTANCE_HIGH
).apply {
    description = "New message notifications"
    enableLights(true)
    lightColor = Color.BLUE
}
notificationManager.createNotificationChannel(channel)
```

| Importance | Behavior | Use For |
|-----------|----------|---------|
| IMPORTANCE_HIGH | Sound + heads-up | Messages, calls...

© platform-design-skills · MIT · ehmo/platform-design-skills

Похожие скиллы

Всё по теме «Мобильная разработка» →
mobile-android-design Seth Hobson Скилл помогает проектировать нативные Android-интерфейсы на Material Design 3 и Jetpack Compose. Подойдёт для экранов, навигации и адаптивных раскладок. CX Codex 24 847установок compose-component-design chrisbanes Скилл для проектирования и ревью переиспользуемых компонентов Jetpack Compose: слоты, модификаторы, размещение корневого layout. CX Codex +1 1 000установок android-development Mindrally Набор правил для ИИ-агента: как писать Android-приложение на Kotlin с чистой архитектурой, MVI и Material 3. Пригодится, когда агент разъезжается по стилю и слоям. CX Codex +1 782установок android-jetpack-compose TheBushidoCollective Помогает агенту писать Android-интерфейсы на Jetpack Compose и выбирать паттерны состояния. Пригодится при работе с composable-функциями и ViewModel. CX Codex +1 2 200установок stream-android GetStream Скилл для работы с Stream Chat, Video и Feeds в Android-проектах на Kotlin: от установки пакетов до экранов чата, звонков и ленты. CX Codex +1 491установок android-jetpack-compose-cursorrules-prompt-file PatrickJS Правило для Cursor, которое задаёт агенту ориентиры по Android-разработке на Jetpack Compose: архитектура, состояние, тесты. ★ 41 011звёзд репо

Из того же набора7

macos-design-guidelines Скилл передаёт агенту правила Apple Human Interface Guidelines для macOS. Он пригодится при вёрстке меню, окон и клавиатурных сокращений на SwiftUI или AppKit. CX Codex +1 4 300установок ios-design-guidelines Проверяет интерфейсы iPhone по правилам Apple HIG при создании и рефакторинге SwiftUI или UIKit. Нужен для навигации, доступности и адаптивной вёрстки. CX Codex +1 1 900установок ipados-design-guidelines Набор правил Apple Human Interface Guidelines для iPad: адаптивные раскладки, многозадачность, боковая навигация и поддержка указателя. CX Codex +1 1 200установок tvos-design-guidelines Скилл загружает в ассистент правила Apple HIG для tvOS. Он помогает проектировать экраны Apple TV с фокусной навигацией под пульт Siri Remote и 10-foot UI. CX Codex +1 1 100установок watchos-design-guidelines Скилл с правилами Apple Human Interface Guidelines для watchOS: подсказывает агенту, как делать глanceable-экраны, Digital Crown и complications. CX Codex +1 1 100установок visionos-design-guidelines Добавляет правила Apple Human Interface Guidelines для Apple Vision Pro в ИИ-агента. Помогает проектировать пространственный интерфейс, жесты и размеры элементов. CX Codex +1 932установок

Спонсоры проекта

Компании, которые помогают держать аналитику открытой