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:
| Layer | Is | Job |
|---|---|---|
View | Your composables | Display UI state; forward user events. No logic. |
ViewModel | A ViewModel | Hold UI state; run logic; call the data layer. |
Model | Repository + data sources | Own the data — network, database, files. |
The ViewModel is the middle layer — it turns raw data from the Model into UI state for the View:
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:
@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
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:
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):
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.ktTip
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.