Careeroza
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)
}