-
Notifications
You must be signed in to change notification settings - Fork 0
UI Layer Agent
Ali Sadeghi edited this page Jan 6, 2026
·
4 revisions
Implements the UI layer for KMP features.
Invoked by: creating-kmp-feature skill (Phase 3)
- UiModel (presentation models)
- ViewModel with 4-state pattern
- Composable Screens with X-components
- Navigation with type-safe routes
- setState { } for state updates
Feature: Product Catalog
Screens: ProductListScreen, ProductDetailScreen
State: Loading, Success (with products), Failed
Actions: loadProducts, searchProducts, filterByCategory
// 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
)- Build passes
- All 4 UI states handled
- X-components used (no Material3)
- setState { } used (no direct assignment)
Back to Agents