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

LayerContainsRules
UIComposables, navigation, Material 3 themeRenders state and sends events
UI state holderViewModel, immutable UI state, event handlersOwns screen state and calls repositories
DataRepositories, DAOs, database, local files, remote sources if anyOwns business data and persistence
DomainUse cases or interactorsAdd 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.