Networking & Loading State in Compose

June 02, 2026 1 min read

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.