Skip to content

UI Layer Agent

Ali Sadeghi edited this page May 28, 2026 · 4 revisions

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

Generates

  • {Feature}UiModel.kt — the single state container: plain UI fields + one UiState<DTO> slot per async op (DTO from data/model/; UiState<Unit> for void ops). No *UiState.kt (Rule 11)
  • {Feature}ViewModel.kt — uses setState { copy() }, exposes val 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-located private @Preview
  • presentation/ui/{Feature}Utils.kt — optional, non-@Composable helpers
  • presentation/navigation/@Serializable Route + NavGraphBuilder.{featurename}(…) extension

Workflow

  1. Follow the UI implementation workflow from _shared/patterns.md.
  2. Load architecture and design-system references on demand.
  3. Implement the single {Feature}UiModel (plain fields + UiState<DTO> slots) — never a separate {Feature}UiState.kt or a presentation-layer DTO mirror.
  4. Implement ViewModel with setState { copy() }, exposing val uiModel.
  5. Implement Screen + ScreenRoot (both required); ScreenRoot takes uiModel: {Feature}UiModel.
  6. Handle all 4 UI states (Uninitialized / Loading / Success / Failed) per async slot.
  7. Implement Navigation with callbacks (no navController in screens).
  8. Self-check (Rule 11): zero import …presentation… under data/; no *UiState.kt file.
  9. Validate: ./gradlew :feature:{featurename}:assembleAndroidMain.

ScreenRoot Pattern (Required)

// 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
}

Navigation Pattern

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.

UI File Organization (Strict Allowlist)

{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/.

Previews (@Preview)

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.

Design System Source

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.

Design-Aware Mode

When invoked from a design-aware orchestrator the agent reads the blueprint's Component Tree from .claude/docs/{featurename}/designs/{featurename}_blueprint.md and:

  1. Applies the X-Component Constraint Check for every component the blueprint uses.
  2. Uses MaterialTheme.colorScheme.{role} exclusively — never raw Color() hex.
  3. Walks the blueprint's Post-Implementation Checklist before declaring done.

See Design-Pipeline.

Output Report

## 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

Clone this wiki locally