Modern Android Architecture
The smallest maintainable Android Compose app has two real layers and one optional layer.
flowchart TD
ui["Compose UI"] --> vm["Screen ViewModel"]
vm --> repo["Repository"]
repo --> room[("Room database")]
repo --> files["Files and exports"]
worker["WorkManager worker"] --> repo
Default Layers
| Layer | Contains | Rules |
|---|---|---|
| UI | Composables, navigation, Material 3 theme | Renders state and sends events |
| UI state holder | ViewModel, immutable UI state, event handlers | Owns screen state and calls repositories |
| Data | Repositories, DAOs, database, local files, remote sources if any | Owns business data and persistence |
| Domain | Use cases or interactors | Add only when logic is reused or complex |
Data And Events
State flows down. Events flow up.
Room Flow -> Repository -> ViewModel UI state -> Compose screen
Compose event -> ViewModel -> Repository -> Room write
This keeps the UI deterministic. A screen should be reproducible from its UI state object.
Activity Boundary
Use one Activity for most v1 apps. It should set content and host navigation. It should not own durable app data.
Android can recreate activities for configuration changes and system pressure. Put durable data in Room and screen state in ViewModel or saved state, not in activity fields.
Repository Boundary
Repositories are not just pass-through DAO wrappers. They are the place to centralize:
- data-source coordination
- import/export rules
- SMS parsing results before persistence
- transaction normalization
- conflict handling
- main-safe API design
Keep Compose and Android UI classes out of repositories so local unit tests stay cheap.
Domain Layer Rule
Do not create a domain layer because diagrams look cleaner. Add it when at least two ViewModels need the same business rule or when a rule becomes too large for a repository.
Good candidates:
- categorizing M-Pesa SMS transactions
- calculating ledger balances across date ranges
- applying export redaction rules
- merging manual edits with imported records
Dependency Management
Start with manual dependency wiring. Move to Hilt when manual wiring gets in the way of testability or Worker construction.
Gotcha: Hilt solves dependency graph wiring. It does not fix unclear ownership. If the repository, ViewModel, and worker all mutate the same state independently, DI will make the bug easier to spread.