ViewModel & State Management

Immutable UI state

Model a screen’s entire state as a single immutable value — often a sealed interface for mutually-exclusive states, or a data class for a form:

sealed interface ArticlesUiState {
    data object Loading : ArticlesUiState
    data class Success(val articles: List<Article>) : ArticlesUiState
    data class Error(val message: String) : ArticlesUiState
}

Exposing state from the ViewModel

Keep the MutableStateFlow private; expose a read-only StateFlow:

class ArticlesViewModel(
    private val repository: ArticleRepository,
) : ViewModel() {

    private val _uiState = MutableStateFlow<ArticlesUiState>(ArticlesUiState.Loading)
    val uiState: StateFlow<ArticlesUiState> = _uiState.asStateFlow()

    fun load() {
        viewModelScope.launch {
            _uiState.value = try {
                ArticlesUiState.Success(repository.articles())
            } catch (e: Exception) {
                ArticlesUiState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

This is exactly the ViewModel in examples/android/, unit-tested in CI.

Collecting in the UI

@Composable
fun ArticlesScreen(viewModel: ArticlesViewModel) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    when (val s = state) {
        ArticlesUiState.Loading -> CircularProgressIndicator()
        is ArticlesUiState.Success -> ArticleList(s.articles)
        is ArticlesUiState.Error -> ErrorText(s.message)
    }
}

Surviving configuration changes

A ViewModel outlives Activity recreation, so the loaded state is not lost on rotation. Use SavedStateHandle for state that must also survive process death:

class SearchViewModel(private val handle: SavedStateHandle) : ViewModel() {
    var query: String
        get() = handle["query"] ?: ""
        set(value) { handle["query"] = value }
}

stateIn for derived flows

Turn a cold flow (e.g. a Room query) into a StateFlow for the UI:

val uiState: StateFlow<ArticlesUiState> =
    repository.articlesFlow()
        .map { ArticlesUiState.Success(it) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), ArticlesUiState.Loading)

Exercises

  1. Add a refresh() to ArticlesViewModel that re-runs load().

  2. Convert a Flow<List<Article>> into a StateFlow<ArticlesUiState> with stateIn.


Previous: Navigation · Next: Local Persistence with Room


Back to top

Kotlin & Android Programming Guide is not affiliated with JetBrains or Google. Content is provided for educational purposes.

This site uses Just the Docs, a documentation theme for Jekyll.