MVVM ViewModel: The Brain of the Operation
Table of Contents
🧠 Theory: What makes a ViewModel a ViewModel?
The Android ViewModel is a class designed with a specific superpower: survive configuration changes.
When you rotate the phone:
Activitydies (onDestroy).Activityis reborn (onCreate).ViewModelstays put.
This makes it the perfect place to hold state (data) and ongoing asynchronous operations. If we didn’t use ViewModel, rotating the screen while loading network data would cancel the request or lose the result.
🏗️ Canonical Structure of a Modern ViewModel
Nowadays, a professional ViewModel follows a strict pattern based on StateFlow.
@HiltViewModel
class ProductViewModel @Inject constructor(
private val getProductsUseCase: GetProductsUseCase // Dependency Injection
) : ViewModel() {
// 1. Backing Property: Private mutable state
private val _uiState = MutableStateFlow<ProductUiState>(ProductUiState.Loading)
// 2. Public immutable state (Read-only)
val uiState: StateFlow<ProductUiState> = _uiState.asStateFlow()
init {
// 3. Automatic initial load
loadProducts()
}
// 4. Public functions (User Intents)
fun refresh() {
loadProducts()
}
private fun loadProducts() {
// 5. viewModelScope: Coroutines tied to VM life
viewModelScope.launch {
_uiState.value = ProductUiState.Loading
getProductsUseCase()
.catch { e ->
_uiState.value = ProductUiState.Error(e.message)
}
.collect { products ->
_uiState.value = ProductUiState.Success(products)
}
}
}
}
🚦 Modeling State
How do we represent the UI? We have two schools of thought.
1. Sealed Interface (Recommended for full screens)
Represents mutually exclusive states. You can’t be loading and successful at the same time.
sealed interface ProductUiState {
object Loading : ProductUiState
data class Success(val products: List<Product>) : ProductUiState
data class Error(val msg: String?) : ProductUiState
}
2. Data Class with Flags (Recommended for complex forms)
Useful when fields are independent.
data class FormUiState(
val email: String = "",
val isEmailValid: Boolean = false,
val isLoading: Boolean = false,
val errors: List<String> = emptyList()
)
⚠️ The “One-Off Events” Problem
How do we handle a Toast or Navigation? They are not state, they are ephemeral events. If you use a StateFlow to show an error Toast, rotating the screen will show the Toast again (because the state is still “Error”).
The Modern Solution: Channels
Use a Channel for “fire and forget” events.
private val _events = Channel<ProductEvent>()
val events = _events.receiveAsFlow()
fun deleteProduct() {
viewModelScope.launch {
try {
repo.delete()
_events.send(ProductEvent.ShowUndoSnackBar) // Consumed once
} catch (e: Exception) {
_events.send(ProductEvent.ShowToast("Error"))
}
}
}
🚫 Anti-Patterns in ViewModels
-
Context in ViewModel: ❌
class MyVM(context: Context)Never. If you rotate the screen, theActivitycontext is destroyed, but the VM stays alive -> Memory Leak. If you need resources, useAndroidViewModel(application)or better, inject a wrapper. -
Exposing MutableState: ❌
val state = MutableStateFlow(...)The View could accidentally modify the state (viewModel.state.value = ...). Breaks unidirectional flow. Always expose immutableStateFloworLiveData. -
Massive Business Logic: The VM is an orchestrator. If you have 50 lines of nested
ifs validating business rules, move it to a Use Case. The VM should be lightweight.
🎯 Conclusion
The ViewModel is the brain of the UI, but it must be a focused brain. Its job is to transform raw data into UI-ready state and handle concurrency. If you keep your ViewModels clean, framework-agnostic, and well-tested, you’ll have won half the architecture battle.
You might also be interested in
StateFlow vs. SharedFlow: A Practical Guide
When to use which? Hot streams in Kotlin Coroutines. How to prevent event loss and ensure UI consistency.
Advanced Kotlin Flow: Operators and Patterns
Level up with Kotlin Flow. Master operators like combine, zip, flatMapLatest, and learn to handle complex reactive streams in Android.
Kotlin Coroutines: The Android Guide
Mastering Kotlin Coroutines on Android. Dispatchers, structured concurrency, and best practices for asynchronous programming.