Phase 3 · Navigation & ArchitectureModule 18~30 min read

Architecture: MVVM

Structure real apps with MVVM and unidirectional data flow — the architecture Google recommends.

What you'll learn

You have all the pieces — composables, state, ViewModels. Architecture is how you arrange them so an app stays understandable as it grows to dozens of screens. Google recommends MVVM with unidirectional data flow, and now that you know ViewModels, it'll click into place.

By the end you'll be able to:

  • Explain why architecture matters
  • Describe the Model–View–ViewModel layers and their jobs
  • Apply unidirectional data flow across a whole screen
  • Structure a real app into layers and packages

Why architecture matters

A small app with everything in one file works — until it doesn't. Without structure, UI code, business rules, and data access tangle together: a change in one place breaks another, nothing can be tested in isolation, and two people can't work without colliding. Architecture is just a set of rules for where code goes, giving you separation of concerns — each part has one job and one reason to change.

The MVVM layers

MVVM splits a screen into three responsibilities:

LayerIsJob
ViewYour composablesDisplay UI state; forward user events. No logic.
ViewModelA ViewModelHold UI state; run logic; call the data layer.
ModelRepository + data sourcesOwn the data — network, database, files.
Each layer depends only on the one below it.

The ViewModel is the middle layer — it turns raw data from the Model into UI state for the View:

TaskListViewModel.kt
class TaskListViewModel(
    private val repository: TaskRepository,   // depends on an abstraction
) : ViewModel() {

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

    fun addTask(title: String) {
        viewModelScope.launch {
            repository.add(Task(title = title))     // delegate data work
        }
    }
}

And the View stays thin — collect the state, render it, forward events. If you see an if deciding business rules inside a composable, it probably belongs in the ViewModel:

TaskListScreen.kt
@Composable
fun TaskListScreen(viewModel: TaskListViewModel) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    // The View: renders state, forwards events. No business logic here.
    TaskListContent(
        state = uiState,
        onAddTask = viewModel::addTask,
    )
}

Key idea

The dependency arrows point one way: View → ViewModel → Model. The ViewModel never imports a composable, and the Model never knows the ViewModel exists. That's what lets you test the ViewModel without any UI, and swap the data source without touching the screen.

Unidirectional data flow

Everything you've learned converges here. State flows down — the ViewModel exposes UI state, the View renders it. Events flow up — the View calls ViewModel functions, which update the state, which flows back down. A single, predictable loop:

ViewModel — holds UI state
↓state down↑events up
View — renders & forwards

Because data only moves in this one loop, you can always answer "why does the screen look like this?" — it's a function of the current UI state, and the only way state changed is through a ViewModel function. No hidden mutations, no surprises.

Structuring a real app

A common, scalable package layout groups code by layer (and, in bigger apps, by feature):

project structure
com.example.tasks/
├── data/
│   ├── local/        Room database, DAOs
│   ├── remote/       Retrofit API
│   └── TaskRepository.kt
├── ui/
│   ├── tasklist/     TaskListScreen + TaskListViewModel
│   ├── taskdetail/   TaskDetailScreen + TaskDetailViewModel
│   └── theme/        Color, Type, Theme
└── MainActivity.kt

Tip

Don't over-engineer a two-screen app — but adopt the layers early. It costs little now and saves you from a painful rewrite later. Everything in Phase 4 (Room, Retrofit, repositories) slots neatly into the data/ layer you just outlined.

Recap & quick check

Key takeaways

  • Architecture gives separation of concerns — each part has one job and one reason to change.
  • MVVM: View (composables) displays state; ViewModel holds state and logic; Model (repository) owns data.
  • Dependencies point one way: View → ViewModel → Model; lower layers never know about higher ones.
  • Unidirectional data flow: state flows down, events flow up, in one predictable loop.
  • Structure code by layer (data/ and ui/), grouping each screen's composable with its ViewModel.

Quick check

1. In MVVM, what is the 'View'?

2. Which way do the layer dependencies point?

3. A composable contains a complex business rule. Where should it go?

4. What is unidirectional data flow?

5. A benefit of separation of concerns is…

That completes Phase 3 — your apps have real structure now! 🎉 In Phase 4 we build the data layer for real: coroutines, Flow, DataStore, Room, and Retrofit.