Mobile
Lesson 4 of 8About 3 min readSuggest an edit

Mobile app architecture

A small app can live in a few screens that fetch data and draw it. Then it grows: two screens show the same data, a bug appears only after rotation, and nothing can be tested without a device. Architecture keeps each piece of code responsible for one thing, so change stays cheap.

Three layers

Most modern mobile apps settle on the same broad split.

  • UI layer: views that draw state and report user actions. Composables on Android, SwiftUI views or view controllers on iOS.
  • State holders: objects that turn data into screen state and handle events, usually a ViewModel.
  • Data layer: repositories that own data from the network, the local database and caches, and hide where it came from.

The view, ViewModel and model (the data) together give the pattern its name: MVVM. Some teams add a domain layer of use cases between ViewModels and repositories. It helps when business rules are shared across screens; otherwise it is ceremony.

Dependencies point one way: UI knows about the ViewModel, the ViewModel knows about repositories, and repositories know nothing about screens.

Unidirectional data flow

In unidirectional data flow, state flows down and events flow up. The ViewModel exposes one immutable state value; the view renders it and sends events back; the ViewModel produces a new state. The view never changes data directly.

data class InboxState(
    val messages: List<Message> = emptyList(),
    val loading: Boolean = false,
    val error: String? = null,
)

class InboxViewModel(
    private val repo: MessageRepository,
) : ViewModel() {
    private val _state = MutableStateFlow(InboxState())
    val state: StateFlow<InboxState> = _state

    fun onRefresh() {
        viewModelScope.launch {
            _state.update { it.copy(loading = true) }
            val error = try {
                repo.refresh()
                null
            } catch (e: IOException) {
                "Couldn't refresh. Check your connection."
            }
            _state.update {
                it.copy(loading = false, error = error)
            }
        }
    }
}

The Compose screen collects state and calls onRefresh(). On iOS the same shape works with an @Observable class (or ObservableObject with @Published on older targets) whose properties SwiftUI observes.

Repositories

A repository is the single entry point for one kind of data. It decides whether to read the database, call the API or both, and it exposes observable data, such as a Kotlin Flow or a Swift AsyncSequence, so screens update when the data changes. This is where the offline-first design from the earlier lesson lives. ViewModels should not know about HTTP clients or SQL.

Dependency injection

Dependency injection means an object receives its collaborators instead of creating them. The InboxViewModel above takes a MessageRepository in its constructor, so a test can pass a fake one.

@Observable
final class InboxModel {
    private let repo: MessageRepository
    var messages: [Message] = []

    init(repo: MessageRepository) { self.repo = repo }
}

Constructor injection by hand is enough for many apps. On Android, Hilt generates the wiring and integrates with ViewModels; Koin is a lighter alternative. On iOS, initialiser injection plus SwiftUI’s environment is common.

Keep navigation decisions out of individual views where you can. Navigation Compose on Android and NavigationStack with a path on iOS both let you describe routes as data. Pass identifiers between screens, not whole objects: the next screen loads what it needs from a repository, which also works after process death and when opened from a deep link.

Keep platform code at the edges

Camera, location, permissions, analytics SDKs and push services are platform details. Wrap each behind a small interface owned by your app, and keep the platform calls in one place. Your ViewModels then depend on LocationProvider, not on the platform’s location API, and you can test them on a laptop in milliseconds.

Habits

  • One immutable state per screen; the view only renders it and sends events.
  • ViewModels never hold references to views, activities or contexts.
  • Repositories own data access; nothing else talks to the network or database.
  • Pass ids through navigation, not objects.
  • Inject dependencies through constructors, and add a DI framework only when manual wiring becomes painful.

Next: Networking in mobile apps

Timeouts, retries, caching, pagination, cancellation, API versioning and payload size on unreliable mobile networks.