MVVM Architecture
๐๏ธ MVVM Architecture
๐ญ MVVM, Named
Model
The data layer โ Room entities and DAOs (Chapter 3), Retrofit API services (Chapter 4), DataStore (Chapter 7) โ all unified behind repositories.
View
Compose composables (Chapter 1) โ purely a function of whatever UiState they're given, with no direct knowledge of where that state came from.
ViewModel
Owns and exposes UiState as StateFlow (Chapter 2), reacting to user actions by calling into the Model layer โ never touching a database or network call directly.
Nothing about this is new material โ it's a name for the shape every chapter's code examples already had. Naming it matters because it turns "how should I structure this new screen?" into a question with a known, repeatable answer.
๐๏ธ The Repository Pattern, Properly
Earlier chapters' repositories each wrapped a single data source. A repository can just as easily combine several, presenting one unified API โ this is the classic offline-first shape: check local data immediately, refresh from the network in the background, and let the local database remain the single source of truth the UI actually observes:
The UI never directly asks "give me the network version" or "give me the cached version" โ it just observes tasks, and the repository decides how that data gets kept fresh. This is exactly why refresh()'s catch block can simply do nothing on failure: the already-displayed cached data from Room is a perfectly reasonable fallback, not an error state the UI needs to handle specially.
๐ซ Why a ViewModel Shouldn't Know About Room or Retrofit Directly
Every ViewModel across this course took a repository as a constructor dependency, never a TaskDao or ApiService directly. This is dependency inversion โ the ViewModel depends on an abstraction (what the repository exposes), not the concrete implementation behind it:
@Binds (a lighter alternative to @Provides, used specifically for "this interface, backed by this implementation" mappings) tells Hilt: whenever something asks for a TaskRepository, hand it a TaskRepositoryImpl. A ViewModel constructor asking for TaskRepository never knows or cares that Room and Retrofit exist underneath โ swapping in a fake implementation for a test only requires changing this one binding, with the ViewModel's own code completely untouched.
Every earlier chapter in this course used a concrete repository class directly (no interface) โ that's a genuinely reasonable simplification for a small app or a learning example. The interface-plus-@Binds version shown here is what a larger, more seriously tested codebase adds once the flexibility (swappable implementations, easier fakes for tests) actually earns its extra ceremony โ not a requirement for every app, every time.
๐ A Note on "Clean Architecture" Layers
Some codebases add a further domain layer between the UI and data layers โ standalone "use case" classes (e.g. GetTasksUseCase, RefreshTasksUseCase) that a ViewModel calls instead of a repository directly, intended to hold business logic that doesn't belong in either the UI or the data layer. This course's examples skip that layer deliberately โ for the scale of app built throughout these chapters, ViewModel-calls-repository-directly is a completely legitimate, widely-used architecture. A domain layer is worth reaching for once business logic (not just data fetching) grows complex enough to want its own dedicated, independently-testable home.
MVVM vs Frontend Architecture
| React (hooks + API layer) | Android MVVM | |
|---|---|---|
| UI layer | Function components | Composables |
| State + logic owner | A custom hook, or a store | ViewModel |
| Data access abstraction | An API client module / service layer | Repository (interface + implementation) |
| Swapping real for fake data | Mock the API module | Swap the @Binds implementation |
๐ป Coding Challenges
Challenge 1: An Offline-First Repository
Write a NoteRepository combining Course 2's Note entity/NoteDao with a hypothetical NoteApiService, exposing notes as a Flow from the DAO and a suspend refresh() that fetches from the API and writes results into the DAO, with a try/catch that silently ignores network failure (falling back to cached data).
Goal: Practice the offline-first repository pattern combining two data sources.
Challenge 2: Interface + @Binds
Extract Challenge 1's NoteRepository into an interface NoteRepository plus an implementation NoteRepositoryImpl, and write a Hilt @Module with an abstract @Binds function mapping the interface to the implementation.
Goal: Practice the dependency-inversion setup, distinct from a plain @Inject-constructor class.
Challenge 3: The Full Stack, One More Time
Write a NoteViewModel depending only on the NoteRepository interface (not the impl), exposing a NotesUiState (notes, isLoading, errorMessage), calling refresh() on init, and a NotesScreen composable collecting and rendering that state โ pulling together everything from Chapters 1 through 8 into one complete, working screen.
Goal: Assemble one final, complete example using every architectural piece from this course.
Architecture & Data closes here having covered Compose, ViewModel/StateFlow, Room, Retrofit, Hilt, coroutine scopes, DataStore, and now the MVVM pattern tying them together. Course 3 (Production & Publishing) shifts focus to shipping this kind of app for real โ testing it, handling background work, securing it, optimizing performance, and getting it into the Play Store.
๐ฏ What's Next
Android Development โ Architecture & Data (Course 2) is complete. Course 3 (Production & Publishing) begins with Jetpack Compose Advanced โ custom layouts, animations, theming, and performance.