Skip to main content

Architecture Guide

This section is the reference companion to the curriculum modules. While each module teaches one slice of the stack, this guide steps back and shows how those slices fit together in a production codebase.

The big picture

A modern Android app is a layered, reactive, dependency-injected system:

Layered Android architecture
PRESENTATION • COMPOSE UIComposables observe StateFlow • emit IntentsVIEWMODELUiState · viewModelScope · SavedStateHandleDOMAIN • USE CASESPure Kotlin · Business rules · Flow / suspendREPOSITORYSingle source of truth · cache + refreshRetrofit · OkHttpNetworkRoom · DataStoreLocalFirebase · RemoteCloud
Each layer depends only on the abstraction below it. Data flows up; intents flow down.

Three principles drive every decision:

🧭

Single Source of Truth

For every piece of data, one place owns it. Everyone else observes.

🔄

Reactive Streams

State propagates automatically via Flow / StateFlow — no manual notification.

🪜

Dependency Inversion

Outer layers depend on inner abstractions. Domain depends on no Android, no library.

Pick the right architecture for the project

Not every app needs full Clean Architecture. Be honest about the size:

App sizeRecommended architecture
Hackathon / proof of conceptMVVM + Repository, single module
Indie app, < 20 screensMVVM + Repository + Hilt, optional :domain module
Production, 20+ screens, teamClean Architecture + modularization + KMP-ready
Multi-platform (Android + iOS)Clean Architecture + KMP :shared module

Over-engineering is the most common mistake. A use case that just calls one repository method adds boilerplate with no benefit. Start lean, evolve toward Clean Architecture as the codebase grows.

State as the contract

A screen has one UI state class that fully describes what to render:

data class ProductDetailUiState(
val isLoading: Boolean = false,
val product: Product? = null,
val isInWishlist: Boolean = false,
val error: String? = null
)

The View renders deterministically from this state. The ViewModel updates it. Tests assert the state transitions. Nothing else changes UI.

For events (one-shot — navigation, snackbar, toast), use a separate SharedFlow<Event>. Don't put events in state — they'll replay on rotation.

Continue reading