TecMeli uses the Model-View-ViewModel (MVVM) architectural pattern in combination with Clean Architecture. MVVM provides a clear separation between UI logic and business logic, making the codebase more maintainable and testable.
TecMeli uses a sealed class to represent all possible UI states.
// core/util/UiState.ktsealed class UiState<out T> { /** Loading state - operation in progress */ object Loading : UiState<Nothing>() /** Success with data */ data class Success<T>(val data: T) : UiState<T>() /** Error state with message and exception */ data class Error( val message: String, val exception: Throwable? = null ) : UiState<Nothing>() /** Empty state - no data to show */ object Empty : UiState<Nothing>()}
ViewModels are easily testable because they depend on abstractions (use cases).
// ui/screen/home/HomeViewModelTest.ktclass HomeViewModelTest { private lateinit var viewModel: HomeViewModel private val mockGetProductsUseCase = mockk<GetProductsUseCase>() @Before fun setup() { viewModel = HomeViewModel(mockGetProductsUseCase) } @Test fun `searchProducts updates state to Loading then Success`() = runTest { // Given val products = listOf( Product(id = "1", title = "Laptop") ) coEvery { mockGetProductsUseCase("laptop") } returns Result.success(products) // When viewModel.searchProducts("laptop") // Then val states = mutableListOf<UiState<List<Product>>>() viewModel.uiState.take(2).toList(states) assertEquals(UiState.Loading, states[0]) assertEquals(UiState.Success(products), states[1]) } @Test fun `blank query sets state to Empty`() = runTest { // When viewModel.searchProducts("") // Then assertEquals(UiState.Empty, viewModel.uiState.value) coVerify(exactly = 0) { mockGetProductsUseCase(any()) } }}
ViewModel holds the single source of truth for UI state. UI never maintains its own state for business data.
// Good: State in ViewModelval uiState by viewModel.uiState.collectAsState()// Bad: State in Composablevar products by remember { mutableStateOf<List<Product>>(emptyList()) }
Expose Immutable State
Public state is always immutable (StateFlow), private state is mutable (MutableStateFlow).
private val _uiState = MutableStateFlow(...)val uiState: StateFlow<...> = _uiState.asStateFlow()
No Business Logic in ViewModel
ViewModels orchestrate, use cases contain logic.
// Good: Delegate to use casefun searchProducts(query: String) { viewModelScope.launch { getProductsUseCase(query) // Use case handles logic }}// Bad: Logic in ViewModelfun searchProducts(query: String) { viewModelScope.launch { val response = repository.search(query) // Too much logic val filtered = response.filter { it.isActive } // ... }}
Use viewModelScope
Always use viewModelScope for coroutines, never GlobalScope or manual coroutine scopes.