Compose Multiplatform Patterns
monta-app/ocpp-emulator
Compose Multiplatform and Jetpack Compose patterns for KMP projects — state management, navigation, theming, performance, and platform-specific UI.
Material Design 3 and Android platform guidelines. An agent skill from ehmo/platform-design-skills.
$ npx skills add ehmo/platform-design-skills --skill android-design-guidelines -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install ehmo/platform-design-skills android-design-guidelines --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/ehmo/platform-design-skills.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/android .claude/skills/android-design-guidelines && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "android-design-guidelines" agent skill from https://github.com/ehmo/platform-design-skills/tree/main/skills/android into .claude/skills/android-design-guidelines/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "android-design-guidelines", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/ehmo/platform-design-skills/tree/main/skills/androidType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add ehmo/platform-design-skills --skill android-design-guidelines -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install ehmo/platform-design-skills android-design-guidelines --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ehmo/platform-design-skills.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/android .agents/skills/android-design-guidelines && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "android-design-guidelines" agent skill from https://github.com/ehmo/platform-design-skills/tree/main/skills/android into .agents/skills/android-design-guidelines/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "android-design-guidelines", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add ehmo/platform-design-skills --skill android-design-guidelines -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install ehmo/platform-design-skills android-design-guidelines --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ehmo/platform-design-skills.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/android .cursor/skills/android-design-guidelines && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "android-design-guidelines" agent skill from https://github.com/ehmo/platform-design-skills/tree/main/skills/android into .cursor/skills/android-design-guidelines/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "android-design-guidelines", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/ehmo/platform-design-skills.git --path skills/android--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add ehmo/platform-design-skills --skill android-design-guidelines -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install ehmo/platform-design-skills android-design-guidelines --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ehmo/platform-design-skills.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/android .gemini/skills/android-design-guidelines && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "android-design-guidelines" agent skill from https://github.com/ehmo/platform-design-skills/tree/main/skills/android into .gemini/skills/android-design-guidelines/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "android-design-guidelines", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install ehmo/platform-design-skills android-design-guidelinesInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add ehmo/platform-design-skills --skill android-design-guidelines -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/ehmo/platform-design-skills.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/android .github/skills/android-design-guidelines && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "android-design-guidelines" agent skill from https://github.com/ehmo/platform-design-skills/tree/main/skills/android into .github/skills/android-design-guidelines/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "android-design-guidelines", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add ehmo/platform-design-skills --skill android-design-guidelines -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install ehmo/platform-design-skills android-design-guidelines --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ehmo/platform-design-skills.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/android .opencode/skills/android-design-guidelines && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "android-design-guidelines" agent skill from https://github.com/ehmo/platform-design-skills/tree/main/skills/android into .opencode/skills/android-design-guidelines/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "android-design-guidelines", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
android-design-guidelinesMaterial Design 3 and Android platform guidelines. An agent skill from ehmo/platform-design-skills.
Android Design Guidelines is an agent skill from ehmo/platform-design-skills. 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.
Its SKILL.md is about 9.7k tokens, which your agent loads only when the skill is triggered. The skill folder holds 4 other files (for example `AGENTS.md`, `metadata.json` and `rules/_sections.md`).
It sits in Mobile, covering Android development. It works with Android and Jetpack Compose. The repository describes itself as: Platform design skill pack: 300+ rules for Apple HIG, Material Design 3, and WCAG 2.2 across iOS, iPadOS, macOS, watchOS, visionOS, tvOS, Android, and Web. The licence is MIT.
10 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit dc2be82. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are kotlin and xml).
From the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
schemas.android.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Android Design Guidelines loads about 9.7k tokens when it runs. Until then it costs about 77 tokens; SKILL.md has 2,813 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from ehmo/platform-design-skills at commit dc2be82, republished under its MIT licence (© ehmo). 2,813 words, ~9,731 tokens.
.claude/skills/android-design-guidelines/SKILL.md (or your agent's skills folder). This skill also uses 3 other files; get the full folder from GitHub.Enable dynamic color derived from the user's wallpaper. Dynamic color is the default on Android 12+ and should be the primary theming strategy.
// 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: 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:
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 |
// 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-patternRules:
on color role for its background (e.g., onPrimary text on primary background).surface and its variants for backgrounds. Never use primary or secondary as large background areas.tertiary sparingly for accent and complementary contrast only.Support both light and dark themes. Respect the system setting by default.
// Compose: Detect system theme
val darkTheme = isSystemInDarkTheme()Rules:
surface color roles which handle this automatically.When branding requires custom colors, provide a seed color and generate tonal palettes using Material Theme Builder.
// 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:
The primary navigation pattern for phones with 3-5 top-level destinations.
// 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:
secondaryContainer color. Do not override this.For medium and expanded screens (tablets, foldables, desktop).
// 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:
For 5+ destinations or complex navigation hierarchies, typically on expanded screens.
// 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:
Android 13+ supports predictive back with an animation preview.
// Compose: Predictive back with BackHandler (androidx.activity.compose)
BackHandler(enabled = true) {
// Called when back is confirmed; navigate back in your nav controller
navController.popBackStack()
}// 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
}
}<!-- AndroidManifest.xml: opt in to predictive back -->
<application android:enableOnBackInvokedCallback="true">Rules:
BackHandler (from androidx.activity.compose) to intercept back events. In View-based apps, implement OnBackInvokedCallback (API 33+) or OnBackPressedCallback (AndroidX) instead of overriding onBackPressed().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.// 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()
}
}| 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 |
Use window size classes for adaptive layouts, not raw pixel breakpoints.
// 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:
WindowSizeClass from material3-window-size-class for responsive layout decisions.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:
Android 15+ enforces edge-to-edge. All apps should draw behind system bars.
// 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:
enableEdgeToEdge() before setContent. Draw behind both status bar and navigation bar.WindowInsets to pad content away from system bars. Scaffold handles this for top bar and bottom bar content automatically.// Compose: Detect fold posture
val foldingFeatures = WindowInfoTracker.getOrCreate(context)
.windowLayoutInfo(context)
.collectAsState(initial = WindowLayoutInfo(emptyList()))Rules:
ListDetailPaneScaffold or SupportingPaneScaffold from Material3 adaptive library for foldable-aware layouts.| 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 |
// 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:
sp units for text sizes to support user font scaling preferences.MaterialTheme.typography, not hardcoded sizes.The FAB represents the single most important action on a screen.
// 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:
primaryContainer color by default. Use tertiaryContainer for secondary screens.ExtendedFloatingActionButton with a label for clarity. Collapse to icon-only on scroll if needed.// 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:
TopAppBar (small) for most screens. Use MediumTopAppBar or LargeTopAppBar for prominent section or screen titles.// 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:
// 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:
// 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:
// 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:
FilterChip for toggling filters, AssistChip for smart suggestions, InputChip for user-entered content (tags), SuggestionChip for dynamically generated suggestions.| 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 |
// 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:
contentDescription (or null if purely decorative).mergeDescendants = true to group related elements into a single TalkBack focus unit (e.g., a list item with icon + text + subtitle).customActions for swipe-to-dismiss or long-press actions so TalkBack users can access them.// 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:
Rules:
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.// 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
}// 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:
focusOrder unless the default is incorrect.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.
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.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)
}Rules:
WindowInsets.systemGestures to detect and avoid gesture conflict zones.// 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:
clickable modifier includes ripple by default.Rules:
HapticFeedbackType.LongPress.// 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 |
| IMPORTANCE_DEFAULT | Sound | Social updates, emails |
| IMPORTANCE_LOW | No sound | Recommendations |
| IMPORTANCE_MIN | Silent, no status bar | Weather, ongoing |
Rules:
IMPORTANCE_HIGH leads users to disable notifications entirely.contentDescription in notification icons for accessibility.Rules:
MessagingStyle for conversations. Include sender name and avatar.BigTextStyle, BigPictureStyle, InboxStyle) for rich content.// Compose: Permission request
val permissionState = rememberPermissionState(Manifest.permission.CAMERA)
if (permissionState.status.isGranted) {
CameraPreview()
} else {
Column {
Text("Camera access is needed to scan QR codes.")
Button(onClick = { permissionState.launchPermissionRequest() }) {
Text("Grant Camera Access")
}
}
}Rules:
// Photo picker: no permission needed
val pickMedia = rememberLauncherForActivityResult(
ActivityResultContracts.PickVisualMedia()
) { uri ->
uri?.let { /* handle selected media */ }
}
pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))Rules:
READ_MEDIA_IMAGES. No permission needed.ACCESS_COARSE_LOCATION (approximate) unless precise location is essential for functionality.// Compose Glance API widget
class TaskWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceTheme {
Column(
modifier = GlanceModifier
.fillMaxSize()
.background(GlanceTheme.colors.widgetBackground)
.padding(16.dp)
) {
Text(
text = "Tasks",
style = TextStyle(fontWeight = FontWeight.Bold,
color = GlanceTheme.colors.onSurface)
)
// Widget content
}
}
}
}
}Rules:
GlanceTheme.system_app_widget_background_radius).<!-- shortcuts.xml -->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="compose"
android:enabled="true"
android:shortcutShortLabel="@string/compose_short"
android:shortcutLongLabel="@string/compose_long"
android:icon="@drawable/ic_shortcut_compose">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="com.example.app"
android:targetClass="com.example.app.ComposeActivity" />
</shortcut>
</shortcuts>Rules:
Rules:
ShareCompat or Intent.createChooser. Provide rich previews with title, description, and thumbnail.Use this checklist to evaluate Android UI implementations:
| Anti-Pattern | Why It Is Wrong | Correct Approach |
|---|---|---|
| Hardcoded color hex values | Breaks dynamic color and dark theme | Use MaterialTheme.colorScheme roles |
Using dp for text size | Ignores user font scaling | Use sp units |
| Custom bottom navigation bar | Inconsistent with platform | Use Material NavigationBar |
| Navigation bar without labels | Violates Material guidelines | Always show labels |
| Dialog for non-critical info | Interrupts user unnecessarily | Use Snackbar or Bottom Sheet |
| FAB for secondary actions | Dilutes primary action prominence | One FAB for the primary action only |
onBackPressed() override | Deprecated; breaks predictive back | Use BackHandler (Compose) or OnBackInvokedCallback (View-based) for predictive back support |
| Touch targets < 48dp | Accessibility violation | Ensure minimum 48x48dp |
| Permission request at launch | Users deny without context | Request in context with rationale |
| Pure black (#000000) dark theme | Eye strain; not Material 3 | Use Material surface color roles |
| Icon-only navigation bar | Users cannot identify destinations | Always include text labels |
| Full-width content on tablets | Wastes space; poor readability | Max width or list-detail layout |
READ_EXTERNAL_STORAGE for photos | Unnecessary since Android 13 | Use Photo Picker API |
| Blocking UI on permission denial | Punishes the user | Graceful degradation |
| Manual color palette selection | Inconsistent tonal relationships | Use Material Theme Builder |
© ehmo, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 3 other files in skills/android of ehmo/platform-design-skills.
Open the folder on GitHubat commit dc2be82
Android Design Guidelines next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Android Design Guidelines this skillehmo/platform-design-skills | 606 | — | ~9.7k | Automated safety check: Pass | MIT | |
| Compose Multiplatform Patternsmonta-app/ocpp-emulator | 180 | 5 repos | ~2k | Automated safety check: Pass | Apache-2.0 | |
| Stylesarindamxd/camerax-android | 132 | 4 repos | ~2.3k | Automated safety check: Pass | Apache-2.0 | |
| Android Compose Patternsmakifbaysal/tasktrooper | 109 | — | ~1.9k | Automated safety check: Pass | Apache-2.0 | |
| Configuring Test Dependenciesskydoves/android-testing-skills | 333 | — | ~4.1k | Automated safety check: Pass | Apache-2.0 | |
| Setting Up Host Vs Device Testsskydoves/android-testing-skills | 333 | — | ~4.8k | Automated safety check: Pass | Apache-2.0 |
monta-app/ocpp-emulator
Compose Multiplatform and Jetpack Compose patterns for KMP projects — state management, navigation, theming, performance, and platform-specific UI.
arindamxd/camerax-android
A skill your agent uses to integrate the Jetpack Compose Styles API into an Android project.
makifbaysal/tasktrooper
A skill your agent uses when building native Android with Jetpack Compose - stateless composables, state hoisting, ViewModel-owned state, edge-to-edge, predictive back, adaptive layout, atomic…
skydoves/android-testing-skills
A skill your agent uses to wire the correct Gradle dependency matrix for Jetpack Compose UI tests.
skydoves/android-testing-skills
A skill your agent uses to choose between host (Robolectric/JVM) and device (instrumentation) tests for Jetpack Compose, and to configure each correctly.
aldefy/compose-skill
Guides Jetpack Compose and Compose Multiplatform work across Android, Desktop, iOS and web, plus Android TV, Material 3 motion, paging, navigation and design-to-code.
ehmo/platform-design-skills
Apple Human Interface Guidelines for Apple TV. An agent skill from ehmo/platform-design-skills.
ehmo/platform-design-skills
Apple Human Interface Guidelines for Mac. An agent skill from ehmo/platform-design-skills.
ehmo/platform-design-skills
Apple Human Interface Guidelines for Apple Watch. An agent skill from ehmo/platform-design-skills.
ehmo/platform-design-skills
Apple Human Interface Guidelines for iPhone. An agent skill from ehmo/platform-design-skills.
ehmo/platform-design-skills
Apple Human Interface Guidelines for iPad. An agent skill from ehmo/platform-design-skills.
ehmo/platform-design-skills
Apple Human Interface Guidelines for Apple Vision Pro. An agent skill from ehmo/platform-design-skills.
Works with
Categories
Material Design 3 and Android platform guidelines. An agent skill from ehmo/platform-design-skills. Android Design Guidelines is an agent skill from ehmo/platform-design-skills. Material Design 3 and Android platform guidelines.
Android Design Guidelines fits situations like: building Android apps with Jetpack Compose; implementing Material You; tasks involving Android UI; compose components.
Run `npx skills add ehmo/platform-design-skills --skill android-design-guidelines -a claude-code`. Or copy the skill folder (skills/android in ehmo/platform-design-skills) into .claude/skills/android-design-guidelines in your project. Claude Code loads it when a task matches its description.
Run `npx skills add ehmo/platform-design-skills --skill android-design-guidelines -a codex`. Or copy the skill folder (skills/android in ehmo/platform-design-skills) into .agents/skills/android-design-guidelines in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add ehmo/platform-design-skills --skill android-design-guidelines -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/android-design-guidelines, .gemini/skills/android-design-guidelines, .github/skills/android-design-guidelines and .opencode/skills/android-design-guidelines in your project.
SKILL.md names no scripts, command-line tools or credentials: Android Design Guidelines is instructions for the agent only.
SKILL.md names 1 domain. In commands or code: schemas.android.com; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Android Design Guidelines is published under the MIT licence (declared in SKILL.md). It allows redistribution, so the full SKILL.md is shown on this page.
About 9.7k tokens (SKILL.md is roughly 39k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Android Design Guidelines: Compose Multiplatform Patterns (monta-app/ocpp-emulator, 180 stars), Styles (arindamxd/camerax-android, 132 stars), Android Compose Patterns (makifbaysal/tasktrooper, 109 stars) and Configuring Test Dependencies (skydoves/android-testing-skills, 333 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
ehmo (a GitHub user) maintains it in ehmo/platform-design-skills, which has 606 GitHub stars. The repository holds 8 skills in this directory. The repository was last updated on March 19, 2026.
Source: ehmo/platform-design-skills on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.