Mobile Development
State Management: Redux, MVI, MVVM
In 2014, Facebook engineers could not fix a bug with unread messages for three months. The counter flickered: 0 -> 1 -> 0. The problem was not in an algorithm - it was in the architecture: dozens of components could simultaneously mutate chat state, creating unpredictable cycles. The solution - Flux and Unidirectional Data Flow - became the foundation for Redux, MVI, and all of modern mobile state management.
- **Instagram Reels** uses a complex state machine for playback: buffering, auto-pause on scroll, resume on return, interruption handling for incoming calls - none of this is manageable without an explicit finite state machine
- **Spotify** on Android migrated from MVP to MVI specifically because of player state complexity: offline mode, smart speaker sync, CrossFade between tracks - each combination required explicit, type-safe state
- **Airbnb** uses Mavericks (formerly MvRx) - an MVI framework on RxJava/Coroutines - for booking form state management with dozens of fields and complex validation
Unidirectional Data Flow
In 2014, Facebook engineers spent three months chasing a bug: the unread messages counter flickered between zero and one with no apparent reason. The root cause was bidirectional data flow - View mutated Model, Model mutated View, View mutated Model again in an infinite loop. Flux and Unidirectional Data Flow (UDF) were invented specifically to break this cycle. The rule is simple: data flows strictly in one direction - Action -> Dispatcher -> Store -> View -> Action. No exceptions, no shortcuts.
UDF makes state predictable: to reproduce any bug, replay the sequence of actions. This is exactly how time-travel debugging in Redux DevTools works. Every state transition is an explicit, serializable event - making debugging deterministic rather than speculative.
What is the key advantage of Unidirectional Data Flow over bidirectional MVC?
MVI: Model-View-Intent
MVI is Redux reimagined for Android with reactive streams in mind. Intent is a user or system action. Model is the immutable screen state. View renders the Model and emits Intents. The key distinction from Redux: state is typically a Kotlin sealed class, making illegal state combinations literally unrepresentable at the type level. Compose + MVI is a natural pairing: Compose recomposes only changed parts, while MVI guarantees State is always consistent.
The standard MVI decomposition separates three concerns: UiState (what to show), UiEffect (one-shot events - navigation, toast), UiEvent (user actions). SharedFlow for effects and StateFlow for state is the canonical combination in 2024. This separation prevents navigation from re-triggering after screen rotation.
Why does MVI use SharedFlow for one-shot effects (navigation, toast) rather than StateFlow?
MVVM with Reactive Bindings
MVVM (Model-View-ViewModel) on iOS and Android is realized today through Combine and Flow respectively. The ViewModel has zero knowledge of the View - it publishes observable state, the View subscribes independently. This delivers testability: ViewModel tests require no UIKit/Compose setup at all. The primary pitfall is memory leaks through retain cycles in Combine: if a ViewModel stores AnyCancellable incorrectly, the cycle never breaks.
MVVM vs MVI: MVVM allows two-way binding - the View can write directly into a @Published property. MVI prohibits this and routes everything through Intent. In practice, MVVM is more flexible, MVI more strict and predictable. SwiftUI + @Observable (Swift 5.9) reduces MVVM boilerplate to an absolute minimum.
What happens if [weak self] is removed from a Combine pipeline stored in the ViewModel's cancellables?
Finite State Machines
Real screens have complex states: loading, success, error, empty list, partial data, offline mode. Managing these with Boolean flags (isLoading: Bool, hasError: Bool, isEmpty: Bool) creates invalid combinations like isLoading = true AND hasError = true simultaneously. A sealed class or enum with associated values makes invalid states literally unrepresentable - the compiler refuses to construct them. This is a finite state machine enforced at the type level.
XState is a popular state machine library for React Native. For Swift there is swift-state-machine or a hand-rolled enum approach. State machines are especially valuable for complex flows: multi-step onboarding, payment processing, WebSocket connections with reconnect logic.
MVVM and MVI are just different names for the same pattern - the choice between them does not matter
MVVM allows two-way binding and direct mutation of state from the View; MVI strictly prohibits this through Intents - an architectural constraint, not a cosmetic difference
In MVVM a view can bind directly to viewModel.username and write to it. In MVI the View only emits Intents; the reducer decides how state changes. At scale with large teams, MVI is more predictable but requires more boilerplate
What problem does representing UI state as a sealed class solve compared to a set of Boolean flags?
Key Ideas
- **Unidirectional Data Flow** breaks circular dependencies: Action -> Reducer -> State -> View -> Action. Time-travel debugging is possible precisely because every change is an explicit, replayable event
- **MVI** specializes UDF for mobile: sealed class UiState + SharedFlow for effects + StateFlow for state. Compose and SwiftUI are natural fits for this model
- **Sealed class state** makes invalid combinations unrepresentable - more important than readability. The compiler enforces exhaustiveness, preventing teams from forgetting to handle new states
Related Topics
State management permeates the entire mobile architecture:
- Clean Architecture for Mobile — ViewModel and State live in the presentation layer of Clean Architecture; Use Cases return Flow/Publisher, which the ViewModel transforms into UiState
- State Management: Unidirectional Data Flow — The foundational UDF concepts introduced there are extended here to production-grade MVI and MVVM patterns
Вопросы для размышления
- When should you choose MVI over MVVM? What characteristics of a screen or feature make MVI the more justified choice?
- How would you design state management for a complex multi-step onboarding flow that can save progress between sessions?
- SharedFlow vs Channel for UiEffects - what is the fundamental difference and when is each preferable?