TecMeli uses a unidirectional data flow architecture powered by Kotlin’s StateFlow and a custom UiState sealed class. This approach ensures predictable, reactive UI updates that are easy to test and reason about.
Data flows in one direction: from user actions through ViewModels to repositories, and back to the UI via StateFlow. This makes the state predictable and easy to debug.
The UiState sealed class represents all possible states of a screen:
sealed class UiState<out T> { /** Indicates that an asynchronous operation is in progress */ object Loading : UiState<Nothing>() /** * Represents a successful operation with data * @property data The content obtained after the successful operation */ data class Success<T>(val data: T) : UiState<T>() /** * Represents a failed operation * @property message User-friendly error description * @property exception Optional technical cause of the error */ data class Error( val message: String, val exception: Throwable? = null ) : UiState<Nothing>() /** Indicates the operation completed successfully but returned no results */ object Empty : UiState<Nothing>()}
Composables collect the StateFlow and react to state changes:
@Composablefun HomeScreen( viewModel: HomeViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() Column(modifier = Modifier.fillMaxSize()) { SearchBar( onSearch = { query -> viewModel.searchProducts(query) } ) when (uiState) { is UiState.Loading -> { CircularProgressIndicator( modifier = Modifier.align(Alignment.CenterHorizontally) ) } is UiState.Success -> { val products = (uiState as UiState.Success).data LazyColumn { items(products) { product -> ProductCard(product) } } } is UiState.Error -> { val error = (uiState as UiState.Error).message ErrorMessage( message = error, onRetry = { /* retry logic */ } ) } is UiState.Empty -> { EmptyState(message = "Busca productos de Mercado Libre") } } }}
Always use collectAsStateWithLifecycle() instead of collectAsState() to ensure the flow collection respects the lifecycle and stops when the app is in the background.
Sealed classes ensure you handle all possible states at compile-time. The Kotlin compiler will warn you if you forget to handle a state in your when expression.
Predictable State
State can only be modified by the ViewModel, and changes flow unidirectionally to the UI. No surprises or race conditions.
Easy Testing
You can easily test state transitions by asserting the values emitted by the StateFlow:
@Testfun `searchProducts emits Loading then Success`() = runTest { val viewModel = HomeViewModel(fakeUseCase) val states = mutableListOf<UiState<List<Product>>>() viewModel.uiState.onEach { states.add(it) }.launchIn(this) viewModel.searchProducts("phone") assertEquals(UiState.Loading, states[1]) assertTrue(states[2] is UiState.Success)}
Automatic Recomposition
Jetpack Compose automatically recomposes UI when StateFlow values change, ensuring your UI is always in sync with your data.
For screens with multiple independent data sources:
data class DashboardState( val user: UiState<User> = UiState.Empty, val products: UiState<List<Product>> = UiState.Empty, val orders: UiState<List<Order>> = UiState.Empty)