Мобильная разработка
State Management: однонаправленный поток данных
Hacker Way, 2014
Инженер Facebook Джедер Петалва представил Flux на конференции F8 в мае 2014 года. Несколькими месяцами позже Дэн Абрамов выпустил Redux на React Europe 2015, упростив Flux до одного Store и чистых reducers. Идея оказалась настолько точной, что Apple и Google независимо пришли к той же архитектуре - SwiftUI Combine и Android StateFlow воспроизводят однонаправленный поток без единой ссылки на Flux.
Значок непрочитанных сообщений показывает 1, но чат пустой. Открываешь снова - снова 1. Этот баг в Facebook Messenger жил месяцами и его не могли починить. Не потому что никто не знал Objective-C. А потому что MVC не был рассчитан на то, чтобы 50 контроллеров делили одно состояние. Решение изменило то, как пишется UI на следующие 10 лет.
- Instagram (Meta): Redux-подобный стейт для лент, Stories, Direct - миллиарды событий в секунду
- Discord: переход с MVC на Flux в 2016 устранил класс race conditions в голосовых каналах
- Shopify Mobile: ViewModel + StateFlow на Android, ObservableObject на iOS - один паттерн, два языка
- Airbnb: отказались от Redux в пользу MobX - тот же принцип однонаправленности, меньше boilerplate
Проблема MVC: почему Facebook сломался
2014 год. Мессенджер Facebook показывает 1 непрочитанное сообщение. Открываешь чат - пусто. Возвращаешься - снова 1. Баг живёт месяцами, появляется и исчезает, никто не может воспроизвести стабильно. Инженеры Facebook ищут причину и находят не баг - а архитектурную катастрофу.
В классическом MVC контроллеры общаются с моделями, модели уведомляют вьюхи, вьюхи иногда трогают модели напрямую, модели зовут другие модели. Граф зависимостей превращается в спагетти. Когда два разных контроллера - NotificationsController и ChatController - обновляют одну и ту же модель UnreadCount в разном порядке, состояние становится непредсказуемым. Race condition не в тредах. Race condition в потоке данных.
Инженеры Facebook описали проблему публично в докладе на F8 2014 - это стало известно как "Hacker Way" манифест. Вместо поиска бага в коде они изменили архитектуру. Принцип оказался простым: данные должны течь в одну сторону, и только в одну.
Тот же принцип позже стал фундаментом Redux (Дэн Абрамов, 2015), MobX (Michel Weststrate, 2015), SwiftUI Combine, Android StateFlow. Один баг в чате Facebook изменил то, как пишется UI на следующее десятилетие.
Почему баг с непрочитанными сообщениями Facebook было трудно воспроизвести?
Flux/Redux: один поток, одна истина
Flux - это не библиотека. Это паттерн с четырьмя ролями: Action, Dispatcher, Store, View. Данные текут строго по кругу, и никогда в обратную сторону. View не трогает Store напрямую. Store не знает про View. Между ними - Action: намерение изменить состояние.
Redux добавил к Flux одну идею: весь стейт приложения - один объект. Один Store. Reducer - чистая функция `(state, action) => newState`. Никаких сайд-эффектов внутри, никакой мутации. Это сделало состояние приложения детерминированным: при одинаковом sequence действий - всегда одинаковый стейт. Time-travel debugging стал возможным.
React Native с Redux - стандарт де-факто для больших приложений. Instagram, Discord, Shopify используют Redux или его производные. Предсказуемость дороже, чем скорость написания. Баги в продакшне дороже, чем boilerplate.
MobX - альтернатива с другим подходом: вместо иммутабельности и reducers - реактивные observable-объекты. Мутировать можно, но через action-decorator. Меньше boilerplate, больше магии. Популярен в командах с Angular-бэкграундом.
Что делает reducer в Redux?
SwiftUI, Compose, StateFlow: один паттерн, три платформы
Когда Apple в 2019 выпустила SwiftUI, а Google в 2021 - Jetpack Compose как стабильную версию, оба фреймворка пришли с одной встроенной идеей: UI как функция от состояния. Не императивно 'обнови label.text', а декларативно 'перерисуй экран, когда state изменился'.
React Native в 2024 году тоже эволюционировал: useState/useReducer для локального состояния, Zustand или Jotai вместо громоздкого Redux для глобального. Принцип тот же - состояние владеет данными, UI их отражает. Направление потока - только сверху вниз.
Ключевое различие платформ: SwiftUI @State - в памяти View, при пересоздании View - стейт сбрасывается. @StateObject (@ObservedObject в родительском контексте) - переживает ре-рендеры. Android ViewModel переживает смену конфигурации (поворот экрана) - именно для этого и создан.
@State в SwiftUI - это то же самое, что @ObservedObject
@State - локальное состояние одного View, создаётся и уничтожается вместе с ним. @ObservedObject - внешний объект-класс, переживает ре-рендеры, может быть shared между View
Путаница дорого обходится: если использовать @State для ViewModel - данные будут сброшены при каждом пересоздании View. Правило: @State для простых примитивов (Bool, Int, String), @StateObject/@ObservedObject для ViewModel и сложных объектов
Почему в Android ViewModel используется MutableStateFlow приватно, а StateFlow публично?
Ключевые идеи
- **Однонаправленный поток** - данные текут Action -> State -> UI, никогда в обратную сторону
- **Single Source of Truth** - одно место хранения стейта, а не десятки разрозненных переменных
- **Иммутабельность** - state не мутируется, создаётся новый объект. Это делает состояние воспроизводимым
- **@State vs @ObservedObject** - локальный vs разделяемый стейт. Смешать - сбрасывать данные при ре-рендерах
- **StateFlow на Android** - MutableStateFlow приватно (только ViewModel пишет), StateFlow публично (View только читает)
Связанные темы
State management строится на архитектурных паттернах и ведёт к более сложным темам UI-архитектуры:
- State Management: MVI, MVVM на Android — MVI и MVVM - конкретные реализации однонаправленного потока для Android
- UIKit и iOS-архитектуры — MVC в UIKit - то, от чего уходит SwiftUI с @State/@ObservedObject
- Design Patterns: GoF — Observer, Command, Memento - паттерны, лежащие в основе Flux/Redux
Вопросы для размышления
- Facebook решил проблему двунаправленного MVC через однонаправленный Flux. Но есть и другой подход - компонентная изоляция: каждый компонент владеет своим стейтом, нет общих моделей. Когда глобальный стейт необходим, а когда достаточно локального?
- Redux требует действие, reducer и selector для каждого изменения - много boilerplate. MobX позволяет мутировать объекты через actions, меньше кода, но больше "магии". Что важнее в production-приложении: предсказуемость или скорость разработки?
- SwiftUI @StateObject переживает ре-рендеры, но не переживает уничтожение View. Android ViewModel переживает rotation. Что происходит со стейтом при force-kill приложения? Где граница между in-memory state и персистентным?
Связанные уроки
- mob-11 — MVI/MVVM на Android строятся поверх этих же принципов
- mob-04 — UIKit и iOS-архитектуры - предшествующий контекст
- prog-13-patterns — Observer pattern - прямой предок реактивных потоков
- se-05 — GoF паттерны в контексте UI-архитектур
- ds-02-cap-theorem — CAP-компромиссы в state: consistency vs availability
- comp-01-intro