Мобильная разработка
State Management: Redux, MVI, MVVM
Facebook в 2014 году не мог починить баг с непрочитанными сообщениями три месяца. Счётчик мигал: 0 -> 1 -> 0. Проблема была не в алгоритме, а в архитектуре: десятки компонентов могли одновременно менять состояние чата, создавая непредсказуемые циклы. Решение - Flux и однонаправленный поток данных - стало основой для Redux, MVI и современного state management.
- **Instagram Reels** использует сложный state machine для управления воспроизведением: буферизация, автопауза при прокрутке, resume при возврате, обработка прерываний звонка - всё это невозможно без явного конечного автомата состояний
- **Spotify** на Android перешёл с MVP на MVI именно из-за сложности состояний плеера: оффлайн-режим, синхронизация с умными колонками, CrossFade между треками - каждая комбинация требовала явного state
- **Airbnb** использует Mavericks (бывший MvRx) - MVI фреймворк на основе RxJava/Coroutines - для управления состоянием форм бронирования с десятками полей и сложной валидацией
Однонаправленный поток данных
Facebook в 2014 году столкнулись с багом: непрочитанные сообщения мигали между нулём и единицей без видимой причины. Причина - двунаправленный поток: View меняла Model, Model меняла View, View снова меняла Model. Flux и однонаправленный поток данных (UDF) были изобретены именно для устранения этой петли. Правило одно: данные текут строго в одну сторону - Action -> Dispatcher -> Store -> View -> Action.
UDF делает состояние предсказуемым: при любом баге достаточно воспроизвести последовательность actions, чтобы точно воссоздать ситуацию. Time-travel debugging в Redux DevTools работает именно на этом принципе.
В чём ключевое преимущество однонаправленного потока данных перед двунаправленным (MVC)?
MVI: Model-View-Intent
MVI - это Redux для Android, переосмысленный с учётом реактивных потоков. Intent (намерение) - это действие пользователя или системы. Model - неизменяемое состояние экрана. View рендерит Model и испускает Intents. Ключевое отличие от Redux: состояние обычно хранится в sealed class Kotlin, что делает невозможным нелегальные комбинации полей. Compose + MVI - это идеальная пара: Compose перерисовывает только изменившиеся части, а MVI гарантирует, что State всегда консистентен.
В MVI принято разделять состояние на три типа: UiState (что показывать), UiEffect (одноразовые события - навигация, toast), UiEvent (действия пользователя). SharedFlow для эффектов и StateFlow для состояния - стандартная комбинация в 2024 году.
Почему в MVI для одноразовых событий (навигация, показ toast) используют SharedFlow, а не StateFlow?
MVVM с реактивными привязками
MVVM (Model-View-ViewModel) на iOS и Android сегодня реализуется через Combine и Flow соответственно. ViewModel не знает о View совсем - она публикует Observable состояние, а View самостоятельно подписывается. Это даёт тестируемость: ViewModel тестируется без UIKit/Compose. Главная ловушка - утечки памяти через retain cycles в Combine: если ViewModel подписывается сама на себя или хранит AnyCancellable в неправильном месте, цикл не разрушается.
MVVM vs MVI: MVVM допускает двустороннее связывание (binding) - View может писать напрямую в @Published свойство. MVI запрещает это и требует Intent. На практике MVVM более гибок, MVI - более строг и предсказуем. SwiftUI + @Observable (Swift 5.9) упрощает MVVM до минимума шаблонного кода.
Что произойдёт, если в Combine pipeline убрать [weak self] и использовать сильный захват?
Конечные автоматы состояний
Реальные экраны имеют сложные состояния: загрузка, успех, ошибка, пустой список, частичные данные, оффлайн-режим. Комбинировать эти состояния через набор флагов (isLoading: Bool, hasError: Bool, isEmpty: Bool) ведёт к невалидным комбинациям вроде isLoading = true И hasError = true одновременно. Sealed class / enum с associated values делает невалидные состояния буквально нетипизируемыми - компилятор не даст их создать. Это конечный автомат на уровне типов.
XState - популярная библиотека для конечных автоматов в React Native. Для Swift - библиотека swift-state-machine или ручная реализация через enum. Конечные автоматы особенно ценны для сложных flow: onboarding, платёжный процесс, WebSocket соединение с reconnect логикой.
MVVM и MVI - это просто разные названия одного паттерна, выбор между ними не важен
MVVM допускает двустороннее связывание и мутацию состояния напрямую из View; MVI строго запрещает это через Intent - архитектурное ограничение, а не косметическое отличие
В MVVM можно написать view.textField.bind(to: viewModel.username) и View сама пишет в ViewModel. В MVI View только испускает Intent, reducer решает как изменить состояние. При больших командах MVI предсказуемее, но требует больше кода.
Какую проблему решает представление UI-состояния через sealed class вместо набора Boolean флагов?
Ключевые идеи
- **Однонаправленный поток** (UDF) ломает циклические зависимости: Action -> Reducer -> State -> View -> Action. Time-travel debugging возможен именно потому, что каждое изменение - явное событие
- **MVI** специализирует UDF для мобильных: sealed class UiState + SharedFlow для эффектов + StateFlow для состояния. Compose и SwiftUI идеально совместимы с этой моделью
- **Sealed class состояния** делают невалидные комбинации нетипизируемыми - это важнее читаемости. Компилятор проверяет exhaustiveness, команда не забывает обработать новые состояния
Связанные темы
State management пронизывает всю мобильную архитектуру:
- Clean Architecture для мобильных — ViewModel и State находятся в presentation layer Clean Architecture; Use Cases возвращают Flow/Publisher, который ViewModel преобразует в UiState
- State Management: однонаправленный поток данных — Базовые концепции UDF, заложенные там, здесь расширяются до production-паттернов MVI и MVVM
Вопросы для размышления
- Когда стоит использовать MVI вместо MVVM? Какие характеристики экрана или фичи делают MVI более оправданным выбором?
- Как бы вы организовали state management для сложного многошагового onboarding с возможностью сохранения прогресса между сессиями?
- SharedFlow vs Channel для UiEffects - в чём принципиальная разница и когда каждый из них предпочтительнее?