WhollySoftware
Back to blogMobile

Managing App State at Scale: Our Take on Unidirectional Data Flow

Wholly Software TeamDecember 5, 20247 min read
Managing App State at Scale: Our Take on Unidirectional Data Flow

We inherited a mid-size iOS app with roughly 40 screens where view controllers directly mutated shared model objects, and a bug in one screen's checkout flow turned out to be caused by a completely unrelated settings screen mutating a shared user object on a background thread. Tracking that down took four days. That project is the reason we standardized on unidirectional data flow across every new app regardless of platform.

The pattern we use is consistent across Compose, SwiftUI, and even our remaining UIKit/legacy Android work: a single state object per screen, actions or intents flowing in from the UI, and a reducer-style function producing the next state as a pure transformation. On Android that's a ViewModel exposing StateFlow; on iOS it's typically an ObservableObject or, more recently, the @Observable macro, feeding a single struct down to SwiftUI views.

The discipline that actually matters isn't the framework, it's banning direct mutation of shared state from anywhere except the designated reducer. We enforce this with code review and, on larger teams, a lint rule that flags any external file mutating a ViewModel's backing state directly rather than going through an intent function. Without that enforcement, teams drift back to convenient-but-untraceable mutation within a few sprints.

For cross-screen state — user session, cart contents, feature flags — we use a small number of app-level state holders injected via dependency injection, rather than a single global store holding everything. A client project that tried a single Redux-style global store for an entire e-commerce app ended up with a state tree so large that every screen re-rendered on unrelated changes; splitting it into scoped stores per feature domain fixed both the performance problem and the mental overhead of reasoning about the whole tree.

Debugging benefits are the real payoff. With unidirectional flow, we can log every intent and resulting state transition, which turned a class of 'this only happens sometimes in production' bugs into reproducible sequences we could replay from a log. That alone has cut our average state-related bug investigation time from days to hours on the apps where we've fully adopted it.

Mobile ArchitectureState ManagementiOSAndroid
Managing App State at Scale: Our Take on Unidirectional Data Flow — Wholly Software