-
Notifications
You must be signed in to change notification settings - Fork 0
UI Layer Agent
Implements the UI/presentation layer for KMP features. Invoked by creating-kmp-feature skill in Phase 4 (Implementation).
Model: sonnet · Color: purple · Allowed tools: Read, Write, Edit, Glob, Grep, ./gradlew
-
{Feature}UiModel.kt— the single state container: plain UI fields + oneUiState<DTO>slot per async op (DTO fromdata/model/;UiState<Unit>for void ops). No*UiState.kt(Rule 11) -
{Feature}ViewModel.kt— usessetState { copy() }, exposesval uiModel: StateFlow<{Feature}UiModel> -
presentation/ui/{Feature}Screen.kt— 5-slot allowlist only (Screen, ScreenRoot, optional state shells) -
presentation/ui/components/— one file per@Composable, including{Feature}Content.kt, each with a co-locatedprivate @Preview -
presentation/ui/{Feature}Utils.kt— optional, non-@Composablehelpers -
presentation/navigation/—@SerializableRoute +NavGraphBuilder.{featurename}(…)extension
- Follow the UI implementation workflow from
_shared/patterns.md. - Load architecture and design-system references on demand.
- Implement the single
{Feature}UiModel(plain fields +UiState<DTO>slots) — never a separate{Feature}UiState.ktor a presentation-layer DTO mirror. - Implement ViewModel with
setState { copy() }, exposingval uiModel. - Implement Screen + ScreenRoot (both required);
ScreenRoottakesuiModel: {Feature}UiModel. - Handle all 4 UI states (Uninitialized / Loading / Success / Failed) per async slot.
- Implement Navigation with callbacks (no
navControllerin screens). - Self-check (Rule 11): zero
import …presentation…underdata/; no*UiState.ktfile. - Validate:
./gradlew :feature:{featurename}:assembleAndroidMain.
// Screen — ViewModel wrapper (NOT tested)
@Composable
fun FeatureScreen(viewModel: FeatureViewModel, onBackClick: () -> Unit) {
val uiModel by viewModel.uiModel.collectAsStateWithLifecycle()
FeatureScreenRoot(
uiModel = uiModel,
onBackClick = onBackClick,
onRetry = viewModel::retry,
)
}
// ScreenRoot — ViewModel-independent (TESTABLE)
@Composable
fun FeatureScreenRoot(
uiModel: FeatureUiModel,
onBackClick: () -> Unit,
onRetry: () -> Unit,
) {
// X-components only; route on uiModel.{slot}State for async slots
}Routes contain only data. The NavGraphBuilder extension wires the callbacks:
@Serializable
data object DashboardRoute
fun NavGraphBuilder.dashboard(
onActionClick: (String) -> Unit,
onBackToDashboard: () -> Unit,
) {
composable<DashboardRoute> {
DashboardScreen(
viewModel = koinViewModel(),
onActionClick = onActionClick,
onBackToDashboard = onBackToDashboard,
)
}
}BaseAppNavHost.kt then simply calls dashboard(...) — never composable<Route> { … } directly. This is what makes integration point #4 a one-liner.
{Feature}Screen.kt accepts only these top-level @Composable fun declarations — nothing else:
| # | Name | Visibility | When |
|---|---|---|---|
| 1 | {Feature}Screen |
public | Always |
| 2 | {Feature}ScreenRoot |
public | Always |
| 3 | LoadingContent |
private | Only if design specifies a dedicated loading screen |
| 4 | FailedContent |
private | Only if design specifies a dedicated failure screen |
| 5 | EmptyContent |
private | Only if design specifies a dedicated empty screen |
Everything else — including {Feature}Content and every sub-component — lives one file per composable under presentation/ui/components/. {Feature}Content is never inlined into Screen.kt. A component's private helpers stay in its own file.
Non-@Composable helpers (formatters, validators, mappers) go in presentation/ui/{Feature}Utils.kt, never under components/.
Generate a co-located private @Preview for every component under components/, wrapped in XTheme:
import androidx.compose.ui.tooling.preview.Preview // CMP 1.11.0+ — NOT org.jetbrains.compose…
@Preview
@Composable
private fun BalanceCardPreview() {
XTheme { BalanceCard(balance = "1,250.00") }
}Requires in the feature build.gradle.kts: implementation(libs.compose.ui.tooling.preview) (commonMain) + androidRuntimeClasspath(libs.compose.ui.tooling). Use @PreviewParameter for multi-variant previews.
Uses X-components from :core:designsystem. The component-mapping reference (using-design-system/references/component-mappings.md) is loaded on demand. No Material3 components in feature files.
When invoked from a design-aware orchestrator the agent reads the blueprint's Component Tree from .claude/docs/{featurename}/designs/{featurename}_blueprint.md and:
- Applies the X-Component Constraint Check for every component the blueprint uses.
- Uses
MaterialTheme.colorScheme.{role}exclusively — never rawColor()hex. - Walks the blueprint's Post-Implementation Checklist before declaring done.
See Design-Pipeline.
## UI Layer Complete: {featurename}
### Files Created
- presentation/{Feature}UiModel.kt
- presentation/{Feature}ViewModel.kt
- presentation/ui/{Feature}Screen.kt
- presentation/ui/{Feature}Utils.kt (if needed)
- presentation/ui/components/{Feature}Content.kt + *.kt (one per component, each with @Preview)
- presentation/navigation/{Feature}Navigation.kt
### ScreenRoot Pattern
✅ {Feature}Screen — ViewModel wrapper (collects viewModel.uiModel)
✅ {Feature}ScreenRoot — ViewModel-independent (takes uiModel: {Feature}UiModel)
### Rules Followed
✅ _uiModel.setState {} used
✅ All 4 UI states handled per async slot
✅ X-components only
✅ ImmutableList for collections
✅ Callback parameters
✅ Single {Feature}UiModel.kt — no {Feature}UiState.kt (Rule 11)
✅ UiState<> slots wrap DTOs from data/model/ — no presentation mirrors (Rule 11)
✅ Build successful
Back to Feature-Development-Agents | Agents