mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 18:39:55 +02:00
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory. Co-authored-by: Junie <junie@jetbrains.com>
247 lines
8.7 KiB
Markdown
247 lines
8.7 KiB
Markdown
# Material 3 Theming & Components
|
||
|
||
## TL;DR Defaults
|
||
|
||
| Concern | Default |
|
||
|---|---|
|
||
| Theme entry point | `MaterialTheme(colorScheme, typography, shapes)` wrapping app content |
|
||
| Dynamic color | Enable on Android 12+; fall back to brand `ColorScheme` on older APIs |
|
||
| Dark/light | Follow system via `isSystemInDarkTheme()`; expose user override if needed |
|
||
| Color pairing | Always pair `primary`/`onPrimary`, `surface`/`onSurface`, `*Container`/`on*Container` |
|
||
| Typography | Use default M3 type scale; override only specific slots for branding |
|
||
| Shapes | Use default M3 shape scale; override per-slot (`small`, `medium`, `large`) |
|
||
| Scaffold | Use `Scaffold` for screens with app bars, FAB, snackbar, or bottom bar |
|
||
| Navigation | `NavigationSuiteScaffold` auto-switches bar/rail by window size |
|
||
| Snackbar | `SnackbarHostState` in Route; show via `Effect` from ViewModel |
|
||
| Bottom sheet | `ModalBottomSheet` with `SheetState`; control via `show()`/`hide()` |
|
||
| Dialog | `AlertDialog` for simple confirm/dismiss; custom `Dialog` for complex content |
|
||
| Adaptive layout | Derive window size class once at app level; pass down as state |
|
||
|
||
## Theming Baseline
|
||
|
||
### Theme Setup
|
||
|
||
```kotlin
|
||
@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,
|
||
shapes = AppShapes,
|
||
content = content
|
||
)
|
||
}
|
||
```
|
||
|
||
### Key Rules
|
||
|
||
- Define `LightColorScheme` and `DarkColorScheme` using `lightColorScheme()` / `darkColorScheme()`.
|
||
- Generate brand colors via [Material Theme Builder](https://m3.material.io/theme-builder) for guaranteed tonal palettes.
|
||
- Dynamic color is Android-only; CMP projects fall back to brand schemes on non-Android targets.
|
||
|
||
## Color Roles and Dark/Light
|
||
|
||
### Role Pairing Rules
|
||
|
||
| Container | Content on it |
|
||
|---|---|
|
||
| `primary` | `onPrimary` |
|
||
| `primaryContainer` | `onPrimaryContainer` |
|
||
| `secondary` | `onSecondary` |
|
||
| `secondaryContainer` | `onSecondaryContainer` |
|
||
| `tertiary` | `onTertiary` |
|
||
| `tertiaryContainer` | `onTertiaryContainer` |
|
||
| `surface` | `onSurface` |
|
||
| `surfaceVariant` | `onSurfaceVariant` |
|
||
| `error` | `onError` |
|
||
| `errorContainer` | `onErrorContainer` |
|
||
|
||
### Accessibility Guardrails
|
||
|
||
- Always use the correct `on*` color for text/icons on a container.
|
||
- Do not mix unrelated pairs (e.g., `tertiaryContainer` background with `primaryContainer` text).
|
||
- M3 tonal palettes guarantee 3:1+ contrast when paired correctly.
|
||
|
||
### Do / Don't
|
||
|
||
| Do | Don't |
|
||
|---|---|
|
||
| `containerColor = primary`, `contentColor = onPrimary` | `containerColor = primary`, `contentColor = tertiaryContainer` |
|
||
| Access colors via `MaterialTheme.colorScheme.*` | Hardcode hex colors in components |
|
||
| Test both light and dark themes | Assume light-only usage |
|
||
|
||
## Typography and Shapes
|
||
|
||
### Typography
|
||
|
||
M3 defines 15 text styles across 5 categories:
|
||
|
||
| Category | Sizes |
|
||
|---|---|
|
||
| Display | `displayLarge`, `displayMedium`, `displaySmall` |
|
||
| Headline | `headlineLarge`, `headlineMedium`, `headlineSmall` |
|
||
| Title | `titleLarge`, `titleMedium`, `titleSmall` |
|
||
| Body | `bodyLarge`, `bodyMedium`, `bodySmall` |
|
||
| Label | `labelLarge`, `labelMedium`, `labelSmall` |
|
||
|
||
**Default**: Use M3 defaults. Override individual slots for brand fonts:
|
||
|
||
```kotlin
|
||
val AppTypography = Typography(
|
||
titleLarge = TextStyle(fontFamily = BrandFont, fontWeight = FontWeight.SemiBold, fontSize = 22.sp)
|
||
)
|
||
```
|
||
|
||
### Shapes
|
||
|
||
M3 shape scale: `extraSmall`, `small`, `medium`, `large`, `extraLarge`.
|
||
|
||
**Default**: Use M3 defaults. Override only when brand requires specific corner radii:
|
||
|
||
```kotlin
|
||
val AppShapes = Shapes(
|
||
medium = RoundedCornerShape(12.dp),
|
||
large = RoundedCornerShape(16.dp)
|
||
)
|
||
```
|
||
|
||
## Component Decision Matrix
|
||
|
||
### Scaffold
|
||
|
||
| Slot | When to use |
|
||
|---|---|
|
||
| `topBar` | Screen has a top app bar |
|
||
| `bottomBar` | Screen has bottom navigation or bottom app bar |
|
||
| `floatingActionButton` | Primary action needs FAB |
|
||
| `snackbarHost` | Screen can show snackbars |
|
||
| `content` | Main screen content; receives `PaddingValues` to apply |
|
||
|
||
**Rule**: Always apply `innerPadding` from `Scaffold` to content root.
|
||
|
||
### Top App Bar
|
||
|
||
| Variant | Use case | Scroll / default |
|
||
|---|---|---|
|
||
| `TopAppBar` (small) | Simple screens, minimal actions | Default: `pinnedScrollBehavior` unless you need collapse |
|
||
| `CenterAlignedTopAppBar` | Single primary action, centered title | Same bar family as small |
|
||
| `MediumTopAppBar` | Moderate navigation, collapsible on scroll | `exitUntilCollapsedScrollBehavior` (also `enterAlwaysScrollBehavior` where needed) |
|
||
| `LargeTopAppBar` | Hero screens, prominent title, collapsible | Same scroll behavior family as medium |
|
||
|
||
### Navigation
|
||
|
||
| Window size | Component |
|
||
|---|---|
|
||
| Compact (phones portrait) | `NavigationBar` (bottom) |
|
||
| Medium/Expanded (tablets, landscape) | `NavigationRail` (side) |
|
||
| Auto-switch | `NavigationSuiteScaffold` |
|
||
|
||
**Default**: Use `NavigationSuiteScaffold` for apps with 3-5 top-level destinations. It adapts automatically.
|
||
|
||
```kotlin
|
||
NavigationSuiteScaffold(
|
||
navigationSuiteItems = {
|
||
destinations.forEach { dest ->
|
||
item(
|
||
selected = currentDest == dest,
|
||
onClick = { currentDest = dest },
|
||
icon = { Icon(dest.icon, contentDescription = null) },
|
||
label = { Text(dest.label) }
|
||
)
|
||
}
|
||
}
|
||
) { DestinationContent(currentDest) }
|
||
```
|
||
|
||
### Bottom Sheet
|
||
|
||
| Type | Use case |
|
||
|---|---|
|
||
| `ModalBottomSheet` | Overlays content, dismissible |
|
||
| `BottomSheetScaffold` | Persistent sheet integrated with screen |
|
||
|
||
**State control**: Use `rememberModalBottomSheetState()` + `SheetState.show()`/`hide()`.
|
||
|
||
**MVI pattern**: ViewModel emits `Effect.ShowSheet`; Route composable calls `sheetState.show()` in `LaunchedEffect`.
|
||
|
||
### Snackbar
|
||
|
||
**Setup**: `SnackbarHostState` remembered in Route; passed to `Scaffold.snackbarHost`.
|
||
|
||
**Pattern**:
|
||
```kotlin
|
||
val snackbarHostState = remember { SnackbarHostState() }
|
||
LaunchedEffect(Unit) {
|
||
viewModel.effects.collect { effect ->
|
||
when (effect) {
|
||
is Effect.ShowSnackbar -> {
|
||
val result = snackbarHostState.showSnackbar(effect.message, effect.actionLabel)
|
||
if (result == SnackbarResult.ActionPerformed) viewModel.onEvent(Event.SnackbarAction)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { /* ... */ }
|
||
```
|
||
|
||
### Dialog
|
||
|
||
| Type | Use case |
|
||
|---|---|
|
||
| `AlertDialog` | Simple title + text + confirm/dismiss buttons |
|
||
| `Dialog` + `Card` | Complex content, forms, custom layouts |
|
||
|
||
**MVI pattern**: Dialog visibility controlled by `state.showDialog: Boolean`. Confirm/dismiss dispatch events.
|
||
|
||
## Adaptive Layout Defaults
|
||
|
||
### Window Size Classes
|
||
|
||
| Class | Width breakpoint | Typical devices |
|
||
|---|---|---|
|
||
| Compact | < 600dp | Phones portrait |
|
||
| Medium | 600dp – 840dp | Tablets portrait, large unfolded |
|
||
| Expanded | ≥ 840dp | Tablets landscape, desktop |
|
||
|
||
**Rule**: Compute `WindowSizeClass` once at app/activity level via `currentWindowAdaptiveInfo()`. Pass derived layout decisions down as state.
|
||
|
||
### Canonical Layouts
|
||
|
||
| Layout | Use case | Compose component |
|
||
|---|---|---|
|
||
| List-detail | Master list + detail pane | `ListDetailPaneScaffold`, `NavigableListDetailPaneScaffold` |
|
||
| Supporting pane | Main content + supplementary info | `SupportingPaneScaffold`, `NavigableSupportingPaneScaffold` |
|
||
| Feed | Grid of browsable content | `LazyVerticalGrid` with `GridCells.Adaptive` |
|
||
|
||
**Default**: For list-detail apps, use `NavigableListDetailPaneScaffold` which handles pane visibility and back navigation.
|
||
|
||
**Adaptive navigation:** read `windowSizeClass` (or related adaptive info) once at the root and pass derived flags (e.g. whether to show a top app bar) into your main screen composable.
|
||
|
||
## M2 to M3 Migration Notes
|
||
|
||
| M2 | M3 |
|
||
|---|---|
|
||
| `Colors` | `ColorScheme` |
|
||
| `lightColors()` / `darkColors()` | `lightColorScheme()` / `darkColorScheme()` |
|
||
| `BottomNavigation` | `NavigationBar` |
|
||
| `BottomNavigationItem` | `NavigationBarItem` |
|
||
| `ModalBottomSheetLayout` | `ModalBottomSheet` |
|
||
| `ModalDrawer` | `ModalNavigationDrawer` |
|
||
| `Scaffold` with `scaffoldState` | `Scaffold` with `snackbarHost` slot |
|
||
| `BackdropScaffold` | `BottomSheetScaffold` or custom |
|
||
| `TopAppBar` elevation | `TopAppBar` with `scrollBehavior` |
|
||
|
||
**Key change**: M3 `Scaffold` no longer has `drawerState`. Use `ModalNavigationDrawer` wrapping `Scaffold` instead.
|