ViewModel & State Management
Why a ViewModel?
State held with remember is lost when you navigate away or the screen is recreated, and it isn’t the right place for business logic or network calls. A ViewModel survives configuration changes, holds your UI state, and talks to repositories — keeping composables focused purely on display.
A state-driven ViewModel
sealed interface UsersUiState {
object Loading : UsersUiState
data class Success(val users: List<User>) : UsersUiState
data class Error(val message: String) : UsersUiState
}
@HiltViewModel
class UsersViewModel @Inject constructor(
private val repo: UserRepository
) : ViewModel() {
private val _state = MutableStateFlow<UsersUiState>(UsersUiState.Loading)
val state: StateFlow<UsersUiState> = _state.asStateFlow()
init { load() }
fun load() {
viewModelScope.launch {
_state.value = UsersUiState.Loading
_state.value = try {
UsersUiState.Success(repo.getUsers())
} catch (e: Exception) {
UsersUiState.Error(e.message ?: "Error")
}
}
}
}
Observing it in Compose
@Composable
fun UsersScreen(viewModel: UsersViewModel = hiltViewModel()) {
val state by viewModel.state.collectAsStateWithLifecycle()
when (val s = state) {
is UsersUiState.Loading -> CircularProgressIndicator()
is UsersUiState.Success -> UserList(s.users)
is UsersUiState.Error -> ErrorView(s.message, onRetry = viewModel::load)
}
}
collectAsStateWithLifecycle() safely collects the flow only while the screen is on-screen, saving battery and work.
One state object, exhaustive when
Modeling the whole screen as a single sealed UiState means the when is exhaustive: you can never forget to handle loading or error, and adding a new state forces you to update the UI.
Common mistakes
- Keeping screen state in
rememberwhen it should live in a ViewModel. - Exposing
MutableStateFlowpublicly — expose the read-onlyStateFlow. - Using
collectwithout lifecycle awareness, wasting resources off-screen.
Summary: Put state and logic in a ViewModel, expose a single read-onlyStateFlowof a sealed UI state, and observe it withcollectAsStateWithLifecycle()for clean, robust screens.