Mobile App Architecture

Mobile apps run in a hostile environment for software design. The operating system can pause the app at any moment, destroy and recreate screens when the device rotates, kill the whole process in the background to reclaim memory, and restart it later expecting the user to find everything as they left it. Networks come and go. And the codebase usually grows faster than on the web, because every screen carries layout, state, navigation, and platform integration.

Good mobile architecture answers three questions consistently: where does state live, how does data flow between the network, local storage, and the screen, and what survives configuration changes and process death. The patterns recommended by Google for Android and widely used on iOS converge on the same shape — layered code, unidirectional data flow, and a local data store as the source of truth.

TL;DR

Quick Example

A layered Android screen in Kotlin: a repository that observes the local database and refreshes from the network, and a view model exposing immutable UI state:

The same idea in Swift with the Observation framework:

Core Concepts

Layers

Dependencies point inward: UI depends on data, never the reverse. Repositories expose data as streams (Flow, AsyncSequence, Combine publishers) and hide whether it came from the database, cache, or network. Add a domain layer when logic is shared across screens or complex enough to test on its own; small apps often do not need one. See Clean Architecture.

Unidirectional Data Flow

A screen renders an immutable UI state object and sends events (taps, text changes, refresh requests) to its view model, which updates state. Because state changes in one place, screens are predictable, easy to test, and easy to restore. Compose and SwiftUI are built for this model. See Mobile State Management.

Offline-First Data

For apps people use on the move, read from the local database (Room, SQLDelight, Core Data, SwiftData, or SQLite) and treat the network as a way to update it. Screens observe the database, so data appears instantly and stays consistent across screens. Writes go to the database and a queue of pending changes synchronized in the background, with conflict handling on the server. See Local-First Software.

Dependency Injection

Constructor injection makes dependencies explicit and swappable in tests. On Android, Hilt (built on Dagger) integrates with view models and navigation; Koin is a lighter alternative. On iOS, initializer injection with protocols, environment values in SwiftUI, or small containers are common. Avoid global singletons that hide dependencies.

Lifecycle and State Restoration

Keep three kinds of state in mind: UI element state (scroll position, expanded panels) in rememberSaveable or @SceneStorage; screen state in view models backed by SavedStateHandle or restoration data; and app data in the database. Test process death explicitly — it is the most common source of "the app forgot what I was doing" bugs.

Navigation

Use the platform's navigation library (Navigation Compose, SwiftUI NavigationStack) with typed routes, and pass identifiers between screens rather than whole objects, so each screen can reload its data after process death. See Mobile Navigation.

Scaling the Codebase

Best Practices

Expose Immutable UI State

Model each screen's state as one immutable object updated only by its view model.

Make the Database the Source of Truth

Observe local data in the UI and refresh it from the network, so the app works offline and stays consistent.

Test Lifecycle Edge Cases

Automate tests for rotation, backgrounding, and process death, not only happy paths.

Keep Platform Code at the Edges

Isolate platform APIs behind interfaces in the data layer, so logic can be tested without devices.

Common Mistakes

Network Calls From Screens

Losing State on Process Death

Mutable State Shared Across Layers

FAQ

What architecture does Google recommend for Android apps?

A layered architecture with a UI layer (UI elements and state holders such as view models), a data layer (repositories and data sources), and an optional domain layer, using unidirectional data flow.

What is unidirectional data flow?

A pattern in which state flows down from a state holder to the UI and events flow up from the UI to the state holder, so state changes happen in one place.

What is process death?

When the operating system kills a backgrounded app's process to reclaim memory. When the user returns, the app restarts and must restore what they were doing from saved state and persisted data.

Should I use MVVM, MVI, or something else?

The labels matter less than the principles: view models or similar state holders, immutable UI state, unidirectional events, and repositories for data. MVI formalizes events and state reducers; MVVM is more flexible.

Should business logic be shared between iOS and Android?

When it is substantial and identical, sharing through Kotlin Multiplatform or a cross-platform framework reduces duplication. Keep UI and platform-specific behavior native unless the team chooses a cross-platform UI framework.

Related Topics

References