When to Use
Use this skill when implementing Compose UI screens for Android and Desktop from specifications:
- •Creating new screens in
:features:<feature>:ui-materialand:features:<feature>:ui-unstyled - •Implementing component-based UI with Material Design 3 and Compose Unstyled
- •Adding @Preview annotations for all composables
- •Following dual-theme design system patterns
- •Building responsive layouts with adaptive components
Do NOT use this skill for:
- •Product/PRD decisions → switch to Product Design Mode
- •Visual design/animations → switch to UI/UX Design Mode
- •Shared ViewModel implementation → switch to KMP Mobile Expert Mode
- •SwiftUI iOS screens → switch to SwiftUI Screen Implementation Mode
- •Koin navigation wiring → switch to KMP Mobile Expert Mode
Mode Detection
FROM_SPEC Mode (Default)
Use when implementing screens from explicit specifications (requirements, mocks, wireframes):
- •Identify required UI states (Loading, Error, Content, Empty)
- •Map specification to component structure
- •Create mock data matching spec requirements
- •Implement with token-based styling
- •Add @Preview for all states
DESIGN_FIRST Mode
Use when no explicit spec exists and you need to design the UI:
- •Reference existing similar screens for patterns
- •Use design tokens from
MaterialTheme.tokensandMaterialTheme.componentTokens - •Apply Material 3 Expressive guidelines
- •Implement dual-theme support (Material + Unstyled)
- •Validate with @Preview for all states
Core Requirements
@Preview Mandatory
Every @Composable function MUST have @Preview annotation:
@Composable
fun MyComponent(modifier: Modifier = Modifier) {
// UI implementation
}
// ✅ CORRECT - Has @Preview
@Preview(name = "Default")
@Composable
private fun MyComponentPreview() {
MyTheme {
MyComponent()
}
}
Preview requirements:
- •Private preview composables for IDE/Studio rendering
- •Named previews with
nameparameter for clarity - •Mock ViewModel for screen-level previews
- •Use
Surfacewrapper for isolated component previews - •Preview all UI states: Loading, Error, Content, Edge cases
Dual-Theme Check
All features require BOTH Material Design 3 and Compose Unstyled implementations:
:features:<feature>:ui-material/ → Material Design 3 UI :features:<feature>:ui-unstyled/ → Compose Unstyled UI
Dual-theme implementation pattern:
// ui-material/PokemonListMaterialScreen.kt
@Composable
fun PokemonListMaterialScreen(
viewModel: PokemonListViewModel,
modifier: Modifier = Modifier,
onPokemonClick: (Pokemon) -> Unit = {}
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
PokemonListMaterialContent(uiState = uiState, /* ... */)
}
// ui-unstyled/PokemonListUnstyledScreen.kt
@Composable
fun PokemonListUnstyledScreen(
viewModel: PokemonListViewModel,
modifier: Modifier = Modifier,
onPokemonClick: (Pokemon) -> Unit = {}
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
PokemonListUnstyledContent(uiState = uiState, /* ... */)
}
Token-Based Styling
ALWAYS use design tokens, never hardcoded values:
// ✅ CORRECT - Use tokens
Card(
modifier = modifier.padding(MaterialTheme.tokens.spacing.medium),
elevation = CardDefaults.cardElevation(
defaultElevation = MaterialTheme.tokens.elevation.level2
)
)
// ❌ WRONG - Hardcoded values
Card(
modifier = modifier.padding(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
)
Mock ViewModel for Previews
Create mock ViewModels for screen-level previews:
// PokemonListScreenPreviews.kt
@Composable
private fun PokemonListContentPreview() {
PokemonTheme {
Surface {
PokemonListMaterialContent(
uiState = PokemonListUiState.Content(
pokemons = persistentListOf(
Pokemon(name = "Bulbasaur", detailUrl = "..."),
Pokemon(name = "Charmander", detailUrl = "...")
),
isLoadingMore = false,
hasMore = true
),
restoredScrollIndex = 0,
restoredScrollOffset = 0,
onLoadMore = {},
onPokemonClick = {},
onScrollPositionChanged = { _, _ -> }
)
}
}
}
Essential Workflows
Workflow 1: Create Feature UI Structure
Create module directories following split-by-layer pattern:
features/<feature>/ui-material/src/commonMain/kotlin/<pkg>/
└── <Feature>MaterialScreen.kt # Main screen
└── <Feature>MaterialScreenPreviews.kt # Preview composables
└── components/ # Reusable components
├── <Feature>Card.kt
├── <Feature>Grid.kt
├── LoadingState.kt
├── ErrorState.kt
└── EmptyState.kt
features/<feature>/ui-unstyled/src/commonMain/kotlin/<pkg>/
└── <Feature>UnstyledScreen.kt # Unstyled variant
└── <Feature>UnstyledScreenPreviews.kt # Preview composables
└── components/ # Unstyled components
Workflow 2: Define Screen Contract
Identify required parameters and callbacks:
// Parameters: ViewModel + modifier + navigation callbacks
@Composable
fun <Feature>MaterialScreen(
viewModel: <Feature>ViewModel,
modifier: Modifier = Modifier,
onItemClick: (Item) -> Unit = {},
onBackClick: () -> Unit = {}
)
Workflow 3: Create UI State Handler
Handle all UI states in main content composable:
@Composable
internal fun <Feature>MaterialContent(
uiState: <Feature>UiState,
onLoadMore: () -> Unit,
onItemClick: (Item) -> Unit,
modifier: Modifier = Modifier
) {
when (uiState) {
is <Feature>UiState.Loading -> LoadingState(modifier)
is <Feature>UiState.Error -> ErrorState(uiState.message, onRetry, modifier)
is <Feature>UiState.Content -> <Feature>List(uiState.items, onItemClick, modifier)
}
}
Workflow 4: Implement Reusable Components
Create isolated components with @Preview:
@Composable
fun <Feature>Card(
item: Item,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier,
elevation = CardDefaults.cardElevation(
defaultElevation = MaterialTheme.tokens.elevation.level2
),
onClick = onClick
) {
Column(
modifier = Modifier.padding(MaterialTheme.tokens.spacing.medium)
) {
// Content
}
}
}
@Preview(name = "Default")
@Composable
private fun <Feature>CardPreview() {
PokemonTheme {
Surface {
<Feature>Card(
item = Item(name = "Sample"),
onClick = {}
)
}
}
}
Workflow 5: Implement Main Screen
Wire ViewModel state to content:
@Composable
fun <Feature>MaterialScreen(
viewModel: <Feature>ViewModel,
modifier: Modifier = Modifier,
onItemClick: (Item) -> Unit = {}
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
<Feature>MaterialContent(
uiState = uiState,
onLoadMore = viewModel::loadNextPage,
onItemClick = onItemClick,
modifier = modifier
)
}
Workflow 6: Add All @Preview Variations
Preview all states and edge cases:
@Preview(name = "Loading")
@Composable
private fun <Feature>LoadingPreview() {
PokemonTheme {
Surface {
<Feature>MaterialContent(
uiState = <Feature>UiState.Loading,
onLoadMore = {},
onItemClick = {},
onScrollPositionChanged = { _, _ -> }
)
}
}
}
@Preview(name = "Error")
@Composable
private fun <Feature>ErrorPreview() {
PokemonTheme {
Surface {
<Feature>MaterialContent(
uiState = <Feature>UiState.Error("Network error"),
onLoadMore = {},
onItemClick = {},
onScrollPositionChanged = { _, _ -> }
)
}
}
}
@Preview(name = "Content")
@Composable
private fun <Feature>ContentPreview() {
PokemonTheme {
Surface {
<Feature>MaterialContent(
uiState = <Feature>UiState.Content(
items = persistentListOf(Item("A"), Item("B"), Item("C")),
isLoadingMore = false,
hasMore = true
),
onLoadMore = {},
onItemClick = {},
onScrollPositionChanged = { _, _ -> }
)
}
}
}
Workflow 7: Validate Implementation
Run primary validation command:
./gradlew :composeApp:assembleDebug test --continue
Critical Guardrails
- •
NEVER skip @Preview annotations - Every @Composable function must have preview for IDE validation and visual testing
- •
NEVER use hardcoded dp values - Always use
MaterialTheme.tokens.spacing,MaterialTheme.tokens.elevation,MaterialTheme.tokens.shapes - •
NEVER create screens without dual-theme support - Both Material and Unstyled implementations are required
- •
NEVER use star imports - Always use explicit imports (enforced by .editorconfig)
- •
NEVER access ViewModel.state directly - Use
collectAsStateWithLifecycle()for lifecycle-aware state consumption - •
NEVER put navigation callbacks in UI state - Pass callbacks as parameters to composable functions
Quick Reference
Validation Commands
| Command | Purpose | When to Run |
|---|---|---|
./gradlew :composeApp:assembleDebug test --continue | Primary validation (Android build + all tests) | Always, before committing |
./gradlew :composeApp:run | Run desktop app | Local UI development |
./gradlew :features:<feature>:ui-material:testDebugUnitTest | Run UI tests for Material variant | After adding UI tests |
Token Reference
| Token Category | Access | Example |
|---|---|---|
| Spacing | MaterialTheme.tokens.spacing.medium | 16.dp |
| Elevation | MaterialTheme.tokens.elevation.level2 | 3.dp |
| Shapes | MaterialTheme.tokens.shapes.large | 24.dp corner |
| Motion | MaterialTheme.tokens.motion.durationMedium | 300ms |
Preview Example Template
@Preview(name = "<State/Component Name>")
@Composable
private fun <Feature><State>Preview() {
PokemonTheme {
Surface {
// Component or Content composable
}
}
}
Component Token Usage
Card(tokens = MaterialTheme.componentTokens.card()) TypeBadge(tokens = MaterialTheme.componentTokens.badge()) AnimatedStatBar(tokens = MaterialTheme.componentTokens.progressBar())
Cross-References
| Document | Purpose | Link |
|---|---|---|
| Architecture + conventions | Master reference for modules, DI, vertical slicing | conventions.md |
| Design tokens | Token system (spacing, elevation, shapes, motion) | design_tokens.md |
| Critical patterns | 6 core patterns (ViewModel, Either, Impl+Factory) | critical_patterns_quick_ref.md |
| Navigation 3 | Modular navigation architecture | navigation.md |
| Testing strategy | @Preview requirements and UI testing | testing_strategy.md |
| Animation guides | Creative UI animations and motion | ui-ux-designer |
| Product requirements | Feature acceptance criteria | prd.md |
Reference Implementations
Pokemon List (Material):
Pokemon List (Unstyled):
Pokemon Detail (Material with animations):