Presentation layer modules containing screens, ViewModels, and UI logic
UI modules implement the presentation layer of the application. Each UI module corresponds to a feature screen and contains ViewModels, Compose UI screens, and UI-specific logic.
Defines the contract for cart screen state and actions:
internal interface CartViewModel : StateViewModel<CartScreenState> { fun updateCartItemQuantity(cartItem: CartItem)}internal data class CartScreenState(val cart: Cart)
The interface extends StateViewModel from the viewmodel library module, providing the state: StateFlow<CartScreenState> property.
2
ViewModel Implementation
From cart-ui/src/main/java/com/denisbrandi/androidrealca/cart/presentation/viewmodel/RealCartViewModel.kt:
internal class RealCartViewModel( private val observeUserCart: ObserveUserCart, private val updateCartItem: UpdateCartItem, private val stateDelegate: StateDelegate<CartScreenState>) : ViewModel(), CartViewModel, StateViewModel<CartScreenState> by stateDelegate { init { stateDelegate.setDefaultState(CartScreenState(Cart(emptyList()))) viewModelScope.launch { observeUserCart().collect { cart -> stateDelegate.updateState { CartScreenState(cart) } } } } override fun updateCartItemQuantity(cartItem: CartItem) { updateCartItem(cartItem) }}
The ViewModel uses delegation via StateDelegate to manage state, keeping the implementation focused on business logic.
class CartUIAssembler( private val cartComponentAssembler: CartComponentAssembler) { @Composable private fun makeCartViewModel(): CartViewModel { return viewModel { RealCartViewModel( cartComponentAssembler.observeUserCart, cartComponentAssembler.updateCartItem, StateDelegate() ) } } @Composable fun CartScreenDestination() { CartScreen(makeCartViewModel()) }}
The CartUIAssembler depends on CartComponentAssembler from the component module. UI assemblers should never create component assemblers - they should receive them via constructor injection.
internal interface MainViewModel : StateViewModel<MainScreenState>internal data class MainScreenState( val wishlistBadge: Int = 0, val cartBadge: Int = 0)
From main-ui/src/main/java/com/denisbrandi/androidrealca/main/presentation/view/navigation/BottomNavRouter.kt:
interface BottomNavRouter { @Composable fun OpenPLPScreen() @Composable fun OpenWishlistScreen() @Composable fun OpenCartScreen()}
The BottomNavRouter interface allows the main-ui module to delegate screen creation to the app module, avoiding direct dependencies on other UI modules.
@Composableinternal fun LoginScreen( viewModel: LoginViewModel, onLoggedIn: () -> Unit) { val state by viewModel.state.collectAsStateWithLifecycle() LaunchedEffect(Unit) { viewModel.viewEvent.collect { event -> when (event) { is LoginViewEvent.NavigateToMain -> onLoggedIn() } } } LoginContent( state = state, onLoginClick = { email, password -> viewModel.login(email, password) } )}
Always handle one-time events (like navigation) using EventViewModel rather than state. This prevents events from being replayed on configuration changes.
internal interface PLPViewModel : StateViewModel<PLPScreenState> { fun loadProducts() fun isFavourite(productId: String): Boolean fun addProductToWishlist(product: Product) fun removeProductFromWishlist(productId: String) fun addProductToCart(product: Product)}internal data class PLPScreenState( val fullName: String, val wishlistIds: List<String> = emptyList(), val displayState: DisplayState? = null)internal sealed interface DisplayState { data object Loading : DisplayState data object Error : DisplayState data class Content(val products: List<Product>) : DisplayState}
Using a sealed interface for DisplayState ensures the UI handles all possible states: Loading, Error, and Content.
internal interface WishlistViewModel : StateViewModel<WishlistScreenState> { fun removeFromWishlist(wishlistItemId: String) fun addToCart(wishlistItem: WishlistItem)}internal data class WishlistScreenState( val wishlistItems: List<WishlistItem>)