Write Composable
Overview
Composables in Alkaa follow a strict three-layer Screen → Loader → Content pattern. All UI uses Kuvio components exclusively, state flows from ViewModel through the Loader, and every composable requires both dark and light previews.
Screen Structure
Every screen has three layers: <Feature>Screen (public, stateless, NavGraph entry point) → <Feature>Loader (internal, injects ViewModel, collects state) → <Feature>Content (internal, stateless, tested with Compose Testing).
→ See references/SCREEN_PATTERNS.md for Kotlin code examples of each layer.
For screens with list→detail relationships, a two-pane adaptive layout is required on wide windows (tablets, desktop). The isSinglePane boolean originates at the NavGraph entry and flows unchanged to the Loader where branching happens.
→ See references/ADAPTIVE_LAYOUTS.md for isSinglePane flow, Loader branching pattern, ListDetailPaneScaffold, and toolbar adaptation.
Rules
| Rule |
Details |
| Kuvio only |
Use KuvioText, KuvioIcon, etc. — never raw Text, Icon, or Material components |
| Modifier param |
Every rendering composable accepts modifier: Modifier = Modifier |
| Paddings |
Always even numbers, multiples of 4 (4, 8, 12, 16, 24 dp) |
| Snackbar |
Use Snackbar, never Toast |
| Adaptive |
Screens must work on landscape, tablets, and desktop |
| Previews |
All composables need both dark and light previews |
| isSinglePane origin |
Always computed at NavGraph entry via currentWindowAdaptiveInfo().windowSizeClass.isSinglePane() |
Red Flags
Stop if you notice:
koinInject() or get() called outside of a Loader
Text(text = ..., style = ...) anywhere in the code
- No
Modifier parameter on a rendering composable
- A single preview without dark/light variants
- Odd padding or padding not a multiple of 4
- A needed Kuvio component doesn't exist → implement with
write-design-system-component first
Common Mistakes
| Mistake |
Fix |
| Injecting ViewModel outside a Loader |
Move all koinViewModel() / koinInject() calls to <Feature>Loader |
Using raw Text(style = ...) |
Use the appropriate Kuvio*Text variant instead |
| Mapping or transforming data inside a composable |
Push logic to the ViewModel; composables only render |
Missing Modifier parameter on a rendering composable |
Add modifier: Modifier = Modifier to every rendering function |
| Single preview without dark/light variants |
Always provide both light and dark @Preview |
| Non-multiple-of-4 padding |
Use multiples of 4 (4, 8, 12, 16, 24 dp) |
Computing isSinglePane inside a composable |
Always compute at NavGraph entry, never inside the screen |
Passing isSinglePane = true to detail pane in ListDetailPaneScaffold |
Side-panel detail always receives false |
Calling navigator.navigateTo() without coroutineScope.launch |
navigateTo is a suspend function — wrap in coroutineScope.launch |
Related Skills
- Missing Kuvio component → use
write-design-system-component skill before implementing the composable
- User-facing strings → use
localization skill
- Testing composable behavior → use
write-ui-tests skill
1---2name: write-composable3description: Use when creating a new Composable or modifying an existing one in the Alkaa project — screen structure, state handling, adaptive layouts, Kuvio usage, or previews.4---56# Write Composable78## Overview910Composables in Alkaa follow a strict three-layer Screen → Loader → Content pattern. All UI uses Kuvio components exclusively, state flows from ViewModel through the Loader, and every composable requires both dark and light previews.1112## Screen Structure1314Every screen has three layers: `<Feature>Screen` (public, stateless, NavGraph entry point) → `<Feature>Loader` (internal, injects ViewModel, collects state) → `<Feature>Content` (internal, stateless, tested with Compose Testing).1516→ See `references/SCREEN_PATTERNS.md` for Kotlin code examples of each layer.1718For screens with list→detail relationships, a two-pane adaptive layout is required on wide windows (tablets, desktop). The `isSinglePane` boolean originates at the NavGraph entry and flows unchanged to the Loader where branching happens.1920→ See `references/ADAPTIVE_LAYOUTS.md` for `isSinglePane` flow, Loader branching pattern, `ListDetailPaneScaffold`, and toolbar adaptation.2122## Rules2324| Rule | Details |25|------|---------|26| **Kuvio only** | Use `KuvioText`, `KuvioIcon`, etc. — never raw `Text`, `Icon`, or Material components |27| **Modifier param** | Every rendering composable accepts `modifier: Modifier = Modifier` |28| **Paddings** | Always even numbers, multiples of 4 (4, 8, 12, 16, 24 dp) |29| **Snackbar** | Use Snackbar, never Toast |30| **Adaptive** | Screens must work on landscape, tablets, and desktop |31| **Previews** | All composables need both dark and light previews |32| **isSinglePane origin** | Always computed at NavGraph entry via `currentWindowAdaptiveInfo().windowSizeClass.isSinglePane()` |3334## Red Flags3536Stop if you notice:37- `koinInject()` or `get()` called outside of a `Loader`38- `Text(text = ..., style = ...)` anywhere in the code39- No `Modifier` parameter on a rendering composable40- A single preview without dark/light variants41- Odd padding or padding not a multiple of 442- A needed Kuvio component doesn't exist → implement with `write-design-system-component` first4344## Common Mistakes4546| Mistake | Fix |47|---------|-----|48| Injecting ViewModel outside a Loader | Move all `koinViewModel()` / `koinInject()` calls to `<Feature>Loader` |49| Using raw `Text(style = ...)` | Use the appropriate `Kuvio*Text` variant instead |50| Mapping or transforming data inside a composable | Push logic to the ViewModel; composables only render |51| Missing `Modifier` parameter on a rendering composable | Add `modifier: Modifier = Modifier` to every rendering function |52| Single preview without dark/light variants | Always provide both light and dark `@Preview` |53| Non-multiple-of-4 padding | Use multiples of 4 (4, 8, 12, 16, 24 dp) |54| Computing `isSinglePane` inside a composable | Always compute at NavGraph entry, never inside the screen |55| Passing `isSinglePane = true` to detail pane in `ListDetailPaneScaffold` | Side-panel detail always receives `false` |56| Calling `navigator.navigateTo()` without `coroutineScope.launch` | `navigateTo` is a suspend function — wrap in `coroutineScope.launch` |5758## Related Skills5960- **Missing Kuvio component** → use `write-design-system-component` skill before implementing the composable61- **User-facing strings** → use `localization` skill62- **Testing composable behavior** → use `write-ui-tests` skill