ViewModel and StateFlow

UI State in a ViewModel

The recommended architecture: the ViewModel exposes one StateFlow<UiState>, the UI renders it and sends events back by calling functions. Data flows down, events flow up (unidirectional data flow).

events: onRetry(), onSearch(q)
   UI  ──────────────────────────────►  ViewModel ──► Repository ──► Room / Retrofit
(Compose) ◄──────────────────────────────    │
         state: StateFlow<ProductsUiState>  viewModelScope (cancelled in onCleared)

Code

sealed interface ProductsUiState {
    data object Loading : ProductsUiState
    data class Success(val products: List<Product>, val query: String = "") : ProductsUiState
    data class Error(val message: String) : ProductsUiState
}

class ProductsViewModel(
    private val repository: ProductRepository,
) : ViewModel() {

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

    init { load() }

    fun load() {
        viewModelScope.launch {
            _uiState.value = ProductsUiState.Loading
            _uiState.value = try {
                ProductsUiState.Success(repository.getProducts())
            } catch (e: IOException) {
                ProductsUiState.Error("Check your internet connection")
            }
        }
    }

    fun addToCart(product: Product) {
        viewModelScope.launch { repository.addToCart(product.id) }
    }

    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val app = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as ShopApp
                ProductsViewModel(app.container.productRepository)
            }
        }
    }
}

Tip: In bigger apps Hilt creates ViewModels: annotate with @HiltViewModel and @Inject constructor(...), then use hiltViewModel() in Compose.

Collecting in the UI

@Composable
fun ProductsRoute(viewModel: ProductsViewModel = viewModel(factory = ProductsViewModel.Factory)) {
    // lifecycle-aware: stops collecting when the app is in the background
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    ProductsScreen(state = state, onRetry = viewModel::load, onAdd = viewModel::addToCart)
}