Skip to content

Architecture of Android Apps

codepath-wiki-review[bot] edited this page May 19, 2026 · 40 revisions

Overview

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.

Recommended app architecture

Google's guide breaks the app into three layers:

  1. 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.
  2. 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 a ViewModel is becoming hard to reason about. See the domain layer guide for when this layer is worth introducing.
  3. 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.

UI layer: ViewModel + StateFlow + Compose

The recommended pattern for a screen is:

  • A ViewModel that survives configuration changes and holds the screen's state. It exposes a single immutable uiState as a StateFlow.
  • 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:

  • StateFlow is the recommended observable for new code. LiveData is still supported for existing Java/View-based codebases, but new Compose-first code should use StateFlow collected with collectAsStateWithLifecycle.
  • The UI state is immutable – the ViewModel produces a new state object on every change. The UI never mutates state directly; it only sends events back to the ViewModel.
  • ViewModels should not hold references to Activity, Context, View, or Resources – doing so leaks memory across configuration changes. Use viewModelScope for coroutines so they're cancelled when the ViewModel is cleared.
  • Use ViewModels at the screen level, not inside reusable components.

Data layer: repositories and data sources

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 suspend function or as a Flow for 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).

Domain layer: use cases (optional)

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 ViewModel is 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.

Threading and dependency injection

  • Use Kotlin coroutines and Flow for asynchronous work; viewModelScope is built into ViewModel. Room, Retrofit, and DataStore expose main-safe suspend/Flow APIs, 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.

Legacy patterns

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 / onClick handlers. The pattern is what most legacy apps drift into by default; it produces large, hard-to-test screens.
  • Model–View–Presenter (MVP) – A Presenter class is paired with each Activity/Fragment (which becomes a passive View interface). MVP was the dominant "clean" pattern in 2016–2018 and pre-dates ViewModel. The official Google architecture samples have since replaced their MVP variants with the modern Compose + ViewModel + repository sample on main.
  • Data-binding MVVM – Layout XML binds directly to a ViewModel via the Data Binding Library. This was the predecessor to today's ViewModel + 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.

References

Official Android architecture documentation:

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.

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.

Clone this wiki locally