Skip to content
ArceApps Logo ArceApps
ES

MVVM ViewModel: The Brain of the Operation

⏱️ 3 min read
MVVM ViewModel: The Brain of the Operation

🧠 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:

  1. Activity dies (onDestroy).
  2. Activity is reborn (onCreate).
  3. ViewModel stays 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.

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
}

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

  1. Context in ViewModel: ❌ class MyVM(context: Context) Never. If you rotate the screen, the Activity context is destroyed, but the VM stays alive -> Memory Leak. If you need resources, use AndroidViewModel(application) or better, inject a wrapper.

  2. Exposing MutableState: ❌ val state = MutableStateFlow(...) The View could accidentally modify the state (viewModel.state.value = ...). Breaks unidirectional flow. Always expose immutable StateFlow or LiveData.

  3. 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
Kotlin October 15, 2025

StateFlow vs. SharedFlow: A Practical Guide

When to use which? Hot streams in Kotlin Coroutines. How to prevent event loss and ensure UI consistency.

Read more
Advanced Kotlin Flow: Operators and Patterns
Kotlin October 15, 2025

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.

Read more
Kotlin Coroutines: The Android Guide
Kotlin October 15, 2025

Kotlin Coroutines: The Android Guide

Mastering Kotlin Coroutines on Android. Dispatchers, structured concurrency, and best practices for asynchronous programming.

Read more