Skip to content

UI Layer Agent

Ali Sadeghi edited this page Jan 6, 2026 · 4 revisions

UI Layer Agent

Implements the UI layer for KMP features.

Invoked by: creating-kmp-feature skill (Phase 3)

Generates

  • UiModel (presentation models)
  • ViewModel with 4-state pattern
  • Composable Screens with X-components
  • Navigation with type-safe routes
  • setState { } for state updates

Example Input

Feature: Product Catalog
Screens: ProductListScreen, ProductDetailScreen
State: Loading, Success (with products), Failed
Actions: loadProducts, searchProducts, filterByCategory

Example Output

// feature/productcatalog/presentation/model/ProductUiModel.kt
data class ProductUiModel(
    val id: String,
    val name: String,
    val priceFormatted: String, // "$99.99"
    val imageUrl: String?
)

fun Product.toUiModel() = ProductUiModel(
    id = id,
    name = name,
    priceFormatted = "$${"%.2f".format(price)}",
    imageUrl = imageUrl
)

// feature/productcatalog/presentation/viewmodel/ProductListViewModel.kt
sealed interface ProductListUiState : UiState {
    data object Uninitialized : ProductListUiState
    data object Loading : ProductListUiState
    data class Success(
        val products: ImmutableList<ProductUiModel>,
        val searchQuery: String = ""
    ) : ProductListUiState
    data class Failed(val error: ErrorResponse) : ProductListUiState
}

class ProductListViewModel(
    private val repository: ProductRepository
) : ViewModel() {
    private val _state = MutableStateFlow<ProductListUiState>(Uninitialized)
    val state: StateFlow<ProductListUiState> = _state.asStateFlow()

    fun loadProducts() {
        viewModelScope.launch {
            setState { Loading }
            when (val result = repository.getProducts()) {
                is Success -> setState {
                    Success(result.data.map { it.toUiModel() }.toImmutableList())
                }
                is Failure -> setState { Failed(result.error) }
            }
        }
    }

    fun searchProducts(query: String) {
        val currentState = state.value
        if (currentState is Success) {
            setState { currentState.copy(searchQuery = query) }
        }
    }
}

// feature/productcatalog/presentation/ui/ProductListScreen.kt
@Composable
fun ProductListScreen(
    viewModel: ProductListViewModel = koinViewModel(),
    onProductClick: (String) -> Unit,
    onBackClick: () -> Unit
) {
    val state by viewModel.state.collectAsState()

    XScaffold(
        title = { XText("Products") },
        onBackClick = onBackClick
    ) { padding ->
        when (val currentState = state) {
            is Uninitialized -> {
                LaunchedEffect(Unit) { viewModel.loadProducts() }
            }
            is Loading -> {
                Box(Modifier.fillMaxSize(), contentAlignment = Center) {
                    XLoadingIndicator()
                }
            }
            is Success -> {
                ProductListContent(
                    products = currentState.products,
                    searchQuery = currentState.searchQuery,
                    onSearchChange = viewModel::searchProducts,
                    onProductClick = onProductClick,
                    modifier = Modifier.padding(padding)
                )
            }
            is Failed -> {
                XErrorView(
                    error = currentState.error,
                    onRetry = viewModel::loadProducts
                )
            }
        }
    }
}

// feature/productcatalog/presentation/navigation/ProductCatalogNavigation.kt
@Serializable
data class ProductListRoute(
    val onProductClick: (String) -> Unit,
    val onBackClick: () -> Unit
)

@Serializable
data class ProductDetailRoute(
    val productId: String,
    val onBackClick: () -> Unit
)

Validates

  • Build passes
  • All 4 UI states handled
  • X-components used (no Material3)
  • setState { } used (no direct assignment)

Back to Agents

Clone this wiki locally