-
Notifications
You must be signed in to change notification settings - Fork 6.2k
Architecture of Android Apps
When first building Android apps, many developers start by putting most of the core business logic in activities or fragments. The challenge is that activities and fragments are deeply tied to the Android framework and its lifecycle, which makes them hard to test and hard to evolve. Without an explicit architecture, screens grow into "god objects" that mix UI rendering, state, navigation, and data access.
Google publishes an official Guide to app architecture that is now the recommended starting point for new Android apps. It is built around a layered architecture, a single source of truth for each piece of data, and a unidirectional data flow (UDF) between layers. The guidance below summarizes that approach and links to canonical documentation for each topic.
Google's guide breaks the app into three layers:
-
UI layer – Renders application data on screen and handles user input. Built with Jetpack Compose (or, in older code, Views) plus a state holder (typically a
ViewModel) that exposes an immutable UI state. -
Domain layer (optional) – Holds reusable or complex business logic in use cases (sometimes called interactors). Add it when the same logic is needed by multiple
ViewModels or when aViewModelis becoming hard to reason about. See the domain layer guide for when this layer is worth introducing. - Data layer – Exposes application data through repositories that wrap one or more data sources (network, database, files, sensors, etc.). Repositories are the single entry point for data and contain the business logic that decides where data comes from and how conflicts are resolved.
Two cross-cutting principles tie the layers together:
- Single source of truth (SSOT) – Each piece of data has exactly one authoritative owner. Other components observe that owner rather than holding their own copy. In offline-first apps the local database is usually the SSOT.
- Unidirectional data flow (UDF) – State flows in one direction (data → state holder → UI), and events flow back the other way (UI → state holder → data). This makes state changes traceable and easier to test. See the UI layer guide for diagrams and examples.
For a complete reference implementation, study Google's Now in Android sample, which applies the guide end-to-end and is the canonical "what does an idiomatic modern Android app look like" sample.
The recommended pattern for a screen is:
- A
ViewModelthat survives configuration changes and holds the screen's state. It exposes a single immutableuiStateas aStateFlow. - A UI state data class that contains everything the UI needs to render the screen.
- A composable that collects state in a lifecycle-aware way and sends user events back to the
ViewModel.
data class DiceUiState(
val firstDieValue: Int? = null,
val secondDieValue: Int? = null,
val numberOfRolls: Int = 0,
)
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
fun rollDice() {
_uiState.update { current ->
current.copy(
firstDieValue = Random.nextInt(1, 7),
secondDieValue = Random.nextInt(1, 7),
numberOfRolls = current.numberOfRolls + 1,
)
}
}
}
@Composable
fun DiceRollScreen(viewModel: DiceRollViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// Render uiState; call viewModel.rollDice() from a button onClick.
}A few things to note:
-
StateFlowis the recommended observable for new code.LiveDatais still supported for existing Java/View-based codebases, but new Compose-first code should useStateFlowcollected withcollectAsStateWithLifecycle. - The UI state is immutable – the
ViewModelproduces a new state object on every change. The UI never mutates state directly; it only sends events back to theViewModel. -
ViewModels should not hold references toActivity,Context,View, orResources– doing so leaks memory across configuration changes. UseviewModelScopefor coroutines so they're cancelled when theViewModelis cleared. - Use
ViewModels at the screen level, not inside reusable components.
Each type of data exposed to the rest of the app should be wrapped by a repository (e.g. NewsRepository, UserRepository). Repositories:
- Hide which data source (network, Room database, DataStore, etc.) the data actually came from.
- Are the only place that decides how to combine and reconcile data from multiple sources.
- Expose data either as a one-shot
suspendfunction or as aFlowfor continuous updates.
class NewsRepository(
private val remote: NewsRemoteDataSource,
private val local: NewsLocalDataSource,
) {
fun observeLatestNews(): Flow<List<Article>> = local.articles()
suspend fun refresh() {
local.save(remote.fetchLatestNews())
}
}Data sources are thin wrappers over a single backend (e.g. Retrofit service, Room DAO). Other layers must go through a repository – they should not depend on data sources directly. See the data layer guide for the full set of patterns (offline-first, caching, conflict resolution).
A use case is a small class that encapsulates one piece of business logic – for example, GetLatestNewsWithAuthorsUseCase combines NewsRepository and AuthorsRepository. Introduce use cases when:
- The same logic is needed by multiple
ViewModels. - A
ViewModelis doing too much and would benefit from breaking the work into named, testable pieces.
Do not create a use case for every repository call – that adds boilerplate without value. See the domain layer guide for the trade-offs.
- Use Kotlin coroutines and
Flowfor asynchronous work;viewModelScopeis built intoViewModel. Room, Retrofit, and DataStore expose main-safesuspend/FlowAPIs, so the call sites stay simple. - Use Hilt for dependency injection in non-trivial apps. Constructor-inject repositories into
ViewModels with@HiltViewModel. Manual DI is acceptable for small apps but quickly becomes painful as the graph grows.
Older Android codebases (and many tutorials still online) use one of three earlier patterns. They are worth recognizing when working in existing code, but new code should follow the architecture guide above:
-
Model–View–Controller (MVC) – Activities or fragments play the role of controller, with most logic in
onCreate/onClickhandlers. The pattern is what most legacy apps drift into by default; it produces large, hard-to-test screens. -
Model–View–Presenter (MVP) – A
Presenterclass is paired with eachActivity/Fragment(which becomes a passiveViewinterface). MVP was the dominant "clean" pattern in 2016–2018 and pre-datesViewModel. The official Google architecture samples have since replaced their MVP variants with the modern Compose +ViewModel+ repository sample onmain. -
Data-binding MVVM – Layout XML binds directly to a
ViewModelvia the Data Binding Library. This was the predecessor to today'sViewModel+StateFlow+ Compose pattern. Compose has largely replaced layout-based data binding for new UIs.
The modern guidance keeps the useful idea from each (separation of UI from business logic, an observable state holder) and replaces the implementation with ViewModel, StateFlow, repositories, and Compose.
Official Android architecture documentation:
- Guide to app architecture – top-level overview of the three layers and the principles behind them.
- UI layer – state holders, UI state, and UDF.
-
State holders and UI state – when to use a
ViewModelvs. a plain state holder. -
State production pipeline – producing
StateFlowwithstateInandWhileSubscribed. - Domain layer – when and how to introduce use cases.
- Data layer – repositories, data sources, SSOT, and offline-first patterns.
- Architecture recommendations – the prioritized list of what to do and what to avoid.
- ViewModel overview – lifecycle, scoping, and best practices.
-
Dependency injection with Hilt – constructor-injecting repositories into
ViewModels.
Reference sample apps maintained by Google:
- Now in Android – the flagship sample that applies the architecture guide end-to-end (Compose, multi-module, Hilt, offline-first).
- Architecture Samples – smaller samples covering individual patterns.
Created by CodePath with much help from the community. Contributed content licensed under cc-wiki with attribution required. You are free to remix and reuse, as long as you attribute and use a similar license.
Finding these guides helpful?
We need help from the broader community to improve these guides, add new topics and keep the topics up-to-date. See our contribution guidelines here and our topic issues list for great ways to help out.
Check these same guides through our standalone viewer for a better browsing experience and an improved search. Follow us on twitter @codepath for access to more useful Android development resources.