Мобильная разработка

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 - в чём принципиальная разница и когда каждый из них предпочтительнее?

Связанные уроки

  • comp-01-intro
State Management: Redux, MVI, MVVM

0

1

Войти