A polished screen handles three states: loading, error and data. Model this as a sealed UI state in the ViewModel and render it in Compose.
UI state in the ViewModel
sealed interface UiState {
object Loading : UiState
data class Success(val items: List<Product>) : UiState
data class Error(val message: String) : UiState
}
class ProductsViewModel(private val api: ApiService) : ViewModel() {
var state by mutableStateOf<UiState>(UiState.Loading); private set
fun load() = viewModelScope.launch {
state = try { UiState.Success(api.products(1)) }
catch (e: Exception) { UiState.Error(e.message ?: "Failed") }
}
}
Render each state
@Composable
fun ProductsScreen(vm: ProductsViewModel) {
LaunchedEffect(Unit) { vm.load() }
when (val s = vm.state) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Error -> Text("Error: ${s.message}")
is UiState.Success -> LazyColumn { items(s.items) { Text(it.title) } }
}
}
Tip: A sealed UiState makes it impossible to forget a state —
when forces you to handle all of them.Summary
Model loading/error/data as a sealed UiState in the ViewModel and render it with when in Compose for a robust, complete screen.