Android 向け UI ガイドライン。Material Design 3 (Expressive)、Jetpack Compose、Dynamic Color、Edge-to-Edge、WindowSizeClass、Predictive Back、Large Screen 対応、TalkBack アクセシビリティ、タッチ・ジェスチャーを確認・適用したいときに使用。Use when: designing or implementing Android app UI with Jetpack Compose or View system; applying Material 3; reviewing Android UI code.
このスキルは Android アプリ向け UI 規約を定義します。 Material Design 3(Material You / Expressive) と Jetpack Compose を中心に、Phone / Foldable / Tablet / ChromeOS まで対応する体験を作るためのルールをまとめています。
参考:
MaterialTheme をカスタマイズ。| カテゴリ | 推奨 |
|---|---|
| UI フレームワーク | Jetpack Compose(androidx.compose.*) |
| デザインシステム | Material 3(androidx.compose.material3) |
| ナビゲーション | Navigation Compose(Type-safe Navigation) |
| 画像読み込み | Coil |
| 非同期 | Coroutines + Flow |
| アダプティブ | Material 3 Adaptive(androidx.compose.material3.adaptive) |
| アニメーション | Compose Animation + Motion |
@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(/* brand colors */)
else -> lightColorScheme(/* brand colors */)
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content,
)
}
| ロール | 用途 |
|---|---|
primary / onPrimary | 主要アクション |
secondary / onSecondary | 補助アクション |
tertiary / onTertiary | コントラスト用アクセント |
surface / onSurface | カード・シート背景 |
surfaceContainer* | 階層化されたコンテナ背景 |
error / onError | エラー・破壊的操作 |
Color(0xFF...))を避け、MaterialTheme.colorScheme.primary を使う。Material 3 タイプスケール:
| スタイル | 用途 |
|---|---|
displayLarge / displayMedium / displaySmall | ヒーロー見出し |
headlineLarge / headlineMedium / headlineSmall | 画面見出し |
titleLarge / titleMedium / titleSmall | セクションタイトル、ダイアログ見出し |
bodyLarge / bodyMedium / bodySmall | 本文 |
labelLarge / labelMedium / labelSmall | ボタン・タブラベル |
Text(
text = "タイトル",
style = MaterialTheme.typography.headlineMedium,
)
sp 単位を使い、dp は使わない(テキスト用)。4, 8, 12, 16, 24, 32, 40, 48 dp を使う。object Spacing {
val xs = 4.dp
val sm = 8.dp
val md = 16.dp
val lg = 24.dp
val xl = 32.dp
}
| 用途 | コンポーネント |
|---|---|
| メインアクション | Button(Filled) / FilledTonalButton / OutlinedButton / TextButton |
| FAB | FloatingActionButton / ExtendedFloatingActionButton |
| トップバー | TopAppBar / CenterAlignedTopAppBar / MediumTopAppBar / LargeTopAppBar |
| ボトムバー | BottomAppBar / NavigationBar |
| ナビゲーション | NavigationBar(≤ 5 項目)/ NavigationRail(Medium+)/ NavigationDrawer(Expanded) |
| カード | Card / ElevatedCard / OutlinedCard |
| リスト | ListItem |
| 入力 | TextField(Filled) / OutlinedTextField |
| 選択 | Checkbox / RadioButton / Switch / Slider |
| フィードバック | Snackbar / LinearProgressIndicator / CircularProgressIndicator |
| ダイアログ | AlertDialog / DatePickerDialog / TimePickerDialog / ModalBottomSheet |
Button(onClick = { /* ... */ }) { Text("保存") }
OutlinedButton(onClick = { /* ... */ }) { Text("キャンセル") }
WindowSizeClass に応じてナビゲーションを切り替える:
| WindowSizeClass | ナビゲーション |
|---|---|
| Compact(幅 < 600dp) | NavigationBar(下部) |
| Medium(600 ≤ 幅 < 840dp) | NavigationRail(左) |
| Expanded(幅 ≥ 840dp) | PermanentNavigationDrawer or NavigationRail |
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
NavigationSuiteScaffold(
navigationSuiteItems = {
items.forEach { item ->
item(
icon = { Icon(item.icon, null) },
label = { Text(item.label) },
selected = item.selected,
onClick = item.onClick,
)
}
},
) {
// content
}
@Serializable data object Home
@Serializable data class Detail(val id: Long)
NavHost(navController, startDestination = Home) {
composable<Home> { HomeScreen(onOpenDetail = { navController.navigate(Detail(it)) }) }
composable<Detail> { backStack ->
val detail: Detail = backStack.toRoute()
DetailScreen(id = detail.id)
}
}
Android 14+ の Predictive Back に対応:
// AndroidManifest
// android:enableOnBackInvokedCallback="true"
PredictiveBackHandler(enabled = hasUnsavedChanges) { progress ->
try {
progress.collect { /* アニメーション進捗 */ }
// ジェスチャー完了 → 戻る処理
} catch (e: CancellationException) {
// キャンセル
}
}
Android 15(API 35)+ は Edge-to-Edge が デフォルト強制。
// Activity
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent { AppTheme { App() } }
}
Scaffold(
topBar = { TopAppBar(...) }, // インセット自動適用
contentWindowInsets = WindowInsets.safeDrawing,
) { padding ->
Column(Modifier.padding(padding)) { ... }
}
// カスタム要素
Box(Modifier.windowInsetsPadding(WindowInsets.systemBars)) { ... }
val navigator = rememberListDetailPaneScaffoldNavigator<Item>()
ListDetailPaneScaffold(
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = { AnimatedPane { ItemList(onSelect = { navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, it) }) } },
detailPane = {
AnimatedPane {
navigator.currentDestination?.content?.let { DetailScreen(it) }
}
},
)
WindowInfoTracker でヒンジ・折り目情報を取得可能。AnimatedVisibility — 要素の出入りAnimatedContent — 内容切替Crossfade — シンプルな切替animate*AsState — 値の補間SharedTransitionLayout {
AnimatedContent(targetState = state) { target ->
if (target) {
Image(
modifier = Modifier.sharedElement(
rememberSharedContentState("image"),
animatedVisibilityScope = this@AnimatedContent,
)
)
}
}
}
FastOutSlowInEasing / EmphasizedEasing を使用。val reduceMotion = LocalAccessibilityManager.current?.let { /* check */ } ?: false
contentDescription を画像・アイコンボタンに設定。装飾画像は null。Icon(
imageVector = Icons.Default.Delete,
contentDescription = "削除",
)
IconButton(onClick = { /* ... */ }) {
Icon(Icons.Default.Share, contentDescription = "共有")
}
Modifier.semantics {
contentDescription = "お気に入りに追加"
role = Role.Button
stateDescription = if (isFavorite) "追加済み" else "未追加"
}
Modifier.minimumInteractiveComponentSize() で保証)。skills/ui-accessibility/SKILL.md 参照。LazyColumn / LazyRow / LazyVerticalGrid を使用。Column + verticalScroll でリスト描画しない。key パラメーターで安定した ID を指定し、不要な再コンポジションを回避。LazyColumn {
items(items, key = { it.id }) { item ->
ItemRow(item)
}
}
crossfade。remember / derivedStateOf を適切に使う。@Stable / @Immutable アノテーションで Compose にヒントを与える。Modifier.imePadding() でキーボード回避。WindowInsets.ime を使用。KeyboardOptions(keyboardType / imeAction / autoCorrect)を適切に設定。Modifier.pointerInput や detectTapGestures / detectDragGestures。Modifier.hoverable でホバー対応。material-icons-extended)を使用。<!-- res/mipmap-anydpi-v26/ic_launcher.xml -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>
composeTestRule)。@Preview でさまざまな設定を網羅:
@Preview(name = "Light", showBackground = true)
@Preview(name = "Dark", uiMode = UI_MODE_NIGHT_YES, showBackground = true)
@Preview(name = "Large Font", fontScale = 2.0f)
@Preview(name = "Tablet", device = "spec:width=1280dp,height=800dp,dpi=240")
@Composable
fun ScreenPreview() {
AppTheme { HomeScreen() }
}
skills/ui-accessibility/SKILL.mdskills/ui-review-checklist/SKILL.md