← All courses

ViewModel & State Management

🗓 May 31, 2026 ⏱ 2 min read

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 remember when it should live in a ViewModel.
  • Exposing MutableStateFlow publicly — expose the read-only StateFlow.
  • Using collect without lifecycle awareness, wasting resources off-screen.
Summary: Put state and logic in a ViewModel, expose a single read-only StateFlow of a sealed UI state, and observe it with collectAsStateWithLifecycle() for clean, robust screens.