100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Kotlin

MVVM in Android

How the Model-View-ViewModel pattern maps onto Compose apps, separating UI state, business logic, and data sources into clean, testable layers.

Architecture & ViewModelIntermediate10 min readJul 8, 2026
Analogies

MVVM in Android

MVVM — Model, View, ViewModel — is the architectural pattern that Android's own Jetpack libraries (ViewModel, LiveData/StateFlow, and the Architecture Components guidance) are built around, and it maps particularly cleanly onto Compose because Compose's declarative 'UI is a function of state' philosophy is exactly what a ViewModel-driven architecture produces. In this pattern, the View (a composable) observes state exposed by a ViewModel and renders it; the ViewModel holds and transforms UI state, delegating actual data operations to Model-layer classes like repositories; and the Model layer talks to databases, network APIs, and other data sources, with no awareness of the UI at all.

🏏

Cricket analogy: MVVM mirrors a franchise's structure: the on-field XI (View) plays what the coach (ViewModel) calls, and the coach relies on scouts and analysts (Model) who never set foot on the pitch themselves, just gather and process data.

The three layers in a Compose app

The View layer is composables — HomeScreen, ProductRow, and so on — whose only job is to render whatever state they're given and forward user events (clicks, text input) upward as function calls. They should contain minimal logic beyond layout and formatting; a composable that reaches into a database or makes a network call directly has broken the separation MVVM is meant to provide. The ViewModel layer exposes UI state (often as a StateFlow<UiState>) and functions for the View to call in response to user actions (onRefresh(), onItemClicked(id)), internally launching coroutines that call into repositories and updating its state based on the results. The Model layer — repositories, data sources, DAOs, API services — contains the actual business and data logic and is completely unaware that Compose exists.

🏏

Cricket analogy: The View is the scoreboard operator who only displays numbers and relays the umpire's signals upward, never deciding a run out themselves; the ViewModel is the third umpire reviewing footage and issuing a ruling; the Model is the raw stump-camera feed.

kotlin
data class ProductListUiState(
    val products: List<Product> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null
)

class ProductListViewModel(
    private val repository: ProductRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow(ProductListUiState(isLoading = true))
    val uiState: StateFlow<ProductListUiState> = _uiState.asStateFlow()

    init {
        loadProducts()
    }

    fun loadProducts() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true, error = null) }
            try {
                val products = repository.getProducts()
                _uiState.update { it.copy(products = products, isLoading = false) }
            } catch (e: IOException) {
                _uiState.update { it.copy(isLoading = false, error = "Failed to load products") }
            }
        }
    }
}

@Composable
fun ProductListScreen(viewModel: ProductListViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    when {
        uiState.isLoading -> LoadingSpinner()
        uiState.error != null -> ErrorMessage(uiState.error, onRetry = viewModel::loadProducts)
        else -> ProductList(products = uiState.products)
    }
}

Why the separation pays off

This layering makes the app more testable and maintainable in concrete ways: the ViewModel can be unit-tested without any Android framework dependency or emulator by supplying a fake repository and asserting on the emitted UiState values; the View can be swapped or redesigned (a different layout, or even a completely different composable) without touching business logic; and the Model layer's repositories can be tested independently of both the UI and the ViewModel's coroutine orchestration.

🏏

Cricket analogy: Just as a bowling coach can be evaluated on video analysis alone without needing a live match, a ViewModel is unit-tested with a fake repository asserting UiState values, no emulator required, while the View (the actual playing XI) can be swapped separately.

A useful heuristic: if you can describe what a composable does without mentioning the network or a database, and describe what the ViewModel does without mentioning padding, colors, or Modifier, the separation is holding. The moment a composable starts calling a Retrofit service directly, or a ViewModel starts importing androidx.compose.ui.graphics, the layers have blurred.

A ViewModel must never hold a reference to a composable, an Activity, a Context tied to a UI lifecycle, or any Compose-specific type — doing so risks memory leaks (the ViewModel can outlive the UI across configuration changes) and violates the entire premise of testing the ViewModel independent of Android UI framework classes.

Unidirectional data flow ties it together

MVVM in Compose is almost always paired with unidirectional data flow: state flows down from ViewModel to View, and events flow up from View to ViewModel as function calls, never the reverse. This one-directional cycle is what keeps a growing app's state changes traceable — there is exactly one place (the ViewModel) that decides what the new state is, rather than state being mutated from many different composables scattered across the tree.

🏏

Cricket analogy: Just as instructions flow one way from captain to fielders while feedback flows back as appeals, never fielders repositioning themselves arbitrarily, MVVM's state flows down from ViewModel to View while events flow up as function calls, never the reverse.

  • MVVM splits an app into View (composables that render state), ViewModel (holds/transforms UI state), and Model (repositories/data sources).
  • Composables should render state and forward events upward as function calls, without directly touching network or database code.
  • ViewModels typically expose a StateFlow<UiState> and public functions the View calls in response to user actions.
  • The Model layer (repositories, DAOs, API services) is fully unaware of Compose or the UI layer.
  • Clean separation makes ViewModels unit-testable without Android framework dependencies via fake repositories.
  • ViewModels must never hold references to composables, Activities, or other UI-lifecycle-bound objects.

Practice what you learned

Was this page helpful?

Topics covered

#Kotlin#AndroidWithJetpackComposeStudyNotes#MobileDevelopment#MVVMInAndroid#MVVM#Android#Three#Layers#StudyNotes#SkillVeris