5.9. Состояние: что это такое и зачем им управлять

Когда мы начинаем разработку нового проекта, довольно быстро возникает вопрос — какое решение для управления состоянием выбрать. Однако прежде чем ответить на него, стоит понять — что вообще такое состояние и как оно появляется.

Мы изучим идею управления состоянием и посмотрим — какие проблемы решают библиотеки вроде BLoC и Redux, и почему часто нельзя обойтись лишь инструментами встроенными во Flutter.

Откуда берется состояние

Flutter

Само понятие состояния пришло к нам из реального, физического мира. Для нас важно знать день сейчас или ночь, светит ли солнце за окном или идет дождь, горит зелёный сигнал светофора или нет.

Тот же светофор — у него есть три понятных состояния: горит зелёный свет, жёлтый или красный. Светофор может перейти из зелёного в жёлтый, но не в красный. Проще простого? К сожалению в реальном мире всё сложнее и светофор может попросту выключиться из-за неполадок электросети или в самых редких случаях вообще исчезнуть.

Состояния можно привести к стандартному виду:

  • Idle — готов к работе.

  • Process — загрузка, обработка.

  • Success — успешно выполненная операция.

  • Error — ошибка во время работы.

В нашем примере со светофором состояния «красный», «жёлтый» и «зелёный» — success (с параметром цвета), отключенный или пропавший светофор — error.

Если бы мы углублялись в детали работы светофора, то можно было бы выделить состояние смены с одного цвета на другой — process. Однако на этом мы уже переходим от понятия состояния к его изменению. Об этом и поговорим далее.

Состояния в приложениях

В наших приложениях происходят тысячи различных процессов: как явные, вроде показа правильных пикселей на экране, так и скрытые, как чистка памяти в фоне.

У всего до мельчайших деталей есть свое состояние — данные оперативной памяти, кэш процессора, напряжение на аккумуляторе или блоке питания. Однако для разработки приложений следует фокусироваться на других, высокоуровневых состояниях.

Состояния можно разделить на два типа:

  • глобальные (англ. App State) — влияющие на всё приложение. Например, информация о том, залогинен ли пользователь

  • локальные (англ. Ephemeral State) — состояния, которые относятся к какому-то компоненту или части экрана.

Для локальных состояний нужны более простые решения управления (например, метод setState), в то время как для глобальных состояний нужны более сложные системы вроде пакетов BLoC и Redux. Их мы изучим в отдельных статьях, а в этой будем мыслить фундаментально и не привязываться к конкретным решениям.

Аргументы за использование стейт-менеджеров :

  • переиспользование состояния — если глобальное состояние вроде статуса авторизации пользователя влияет на многие экраны и процессы приложения, его потребителями будут сразу несколько сущностей, и хранить состояние в простом виджете становится неудобно. В таком случае, например, нельзя получить доступ к этому состоянию из бизнес логики.

  • размер состояния — если состояние состоит из множества полей и отдельные части приложения требуют только одно или несколько из них, следует использовать некие фильтры или мапперы состояния. Если же реагировать на смену любого поля в состоянии (как это делается в setState), многие виджеты будут перестраиваться без необходимости и тратить ресурсы устройства впустую.

Аргументы за использование встроенных инструментов (setState):

  • локальность состояния — если цвет вашей кнопки меняется в зависимости от того, нажимает ли на нее пользователь, следует использовать инструменты UI слоя Flutter и не прибегать к лишней передаче данных в отдельный класс для управления состоянием. Так приложение использовало бы больше ресурсов в виде памяти и циклов процессора без преимуществ для разработчика. Если состояние относится строго к вашему виджету и его не требуется отслеживать снаружи, нет необходимости создавать отдельные классы.

Подробнее о типах состояний можно почитать в документации Flutter.

Теперь, обладая знаниями о состояниях, мы можем приступить к обсуждению управления состоянием.

Что значит управлять состоянием

В выражении «управление состоянием» слово «управление» важнее слова «состояние». В современных приложениях тысячи небольших состояний и кусочков данных на которых строится бизнес-логика и интерфейс.

У многих из них есть ограничения: минимальные и максимальные значения, запрет изменений в определенные моменты. Иногда состояния и источники данных рекурсивно влияют друг на друга, так что изменение одного может повлечь смену еще десятка.

Управление состоянием даёт нам правила и инструменты для того, чтобы управлять этим хаосом и создавать порядок. Появляются геттеры и сеттеры состояний, стримы, на которые может подписаться интерфейс, проверки, запрещающие установить неверное состояние.

На практике можно положить все состояния в глобальные переменные или в некий словарь, сделать ему геттеры и сеттеры, затем слушать его изменения. На первый взгляд кажется, что этого достаточно — и различные стейт-менеджеры вроде BLoC или Redux нам ни к чему. Но только на первый взгляд.

Стейт-менеджмент против анархии

Несмотря на то, что готовые решения для управления состоянием имеют свои внутренние «философии» управления данными, все они пытаются сделать одно и то же — создать правила, которые ограничат хаос, и помогут нам создать стабильную систему.

Идея класть состояния в переменные в виджетах или даже в глобальный словарь может сработать, но начнут появляться проблемы с различными паттернами и правилами.

Давайте рассмотрим, какие паттерны относятся к управлению состоянием:

  • Single Responsibility — центральный менеджер отвечает за состояние и оповещение изменений. Не заставляем виджет и управлять UI и иметь бизнес логику.

  • Open-Closed principle — часть логики должна оставаться только в виджете (секунды таймера, процент выполнения анимации), в то время как абстрактное состояние (idle/success) можно показать другим

  • Inversion of Control — правильное управление контролем данных, не давать менять состояние «посторонним» модулям

Как видите, если не игнорировать паттерны, возникает потребность в некой системе, требования к которой описаны ниже. Пренебрежение этими принципами ведёт к растущей сложности кода, проблемам с поддержкой и появлению багов.

  • инкапсуляция — делать разные классы для хранения разных состояний, ограничивать контроль и доступ за счет интерфейсов и скоупов.

  • чистая архитектура — делить низкоуровневые состояния (могут жить в виджете) от состояний бизнес логики (должны быть в классах для бизнес логики).

  • правила мутации состояния — ограничивать набор состояний и переходы между ними. Об этом ниже.

О чистой архитектуре и паттерне IoC мы рассказали в отдельных параграфах архитектурной главы. А для решения проблемы правил мутации существует отличная математическая модель — конечный автомат.

Конечный автомат как идеальный контроль состояния

Конечный автомат (англ. Final State Machine) — система, которая имеет конечный набор состояний, переходов между ними.

Она может принимать лишь одно состояние в любой момент времени и имеет начальное состояние (или начальный переход). К определению конечного автомата очень близок BLoC, так что если он знаком вам, представьте, что состояния из него это состояния конечного автомата, а события это переходы.

Если абстрагироваться от идей инкапсуляции и контроля управлением, конечный автомат идеально подходит для поиска уязвимостей в своих системах и для проектирования.

Рассмотрим наш светофор:

Flutter

Разрешенные состояния это Red, Green, Yellow, Off и Error. А переходы — это:

  • 4 System Fault из любого не Error-состояния в Error

  • Reset, который ведет из Error в Off

  • Из Red в Green

  • Из Green в Yellow

  • Из Yellow в Red

Как видите, входная точка в систему это переход в Off. После этого мы можем сменить состояние в Error или Red. Светофор не может загореться красным сразу после зелёного, он обязан перейти в жёлтый (или сломаться). Если бы светофор мнгновенно менялся с зелёного на красный, водители бы не успевали вовремя тормозить и попадали бы в аварии. Аналогично, непредусмотренные переходы в вашей системе могут привести к нежелательным последствиям.

Если ваша система принимает больше состояний, чем idle, process, success и error, попробуйте нарисовать подобную схему и проверить, из каких состояний в какие вы можете переходить, а в какие нет.

Например, если ваша система покупки в интернет магазине может перейти из пустой корзины к оплате, вы в лучшем случае просто забыли рассмотреть пограничный случай с нулевой суммой оплаты, а в худшем оставили пользователям нерабочую систему.

Теперь давайте создадим на основе нашего светофора класс для управления состоянием с помощью Dart.

Как использовать конечный автомат в коде

Для начала создадим разрешённые состояния. В данном случае для этого достаточно enum.

1enum TrafficLightState {
2  off,
3  red,
4  green,
5  yellow,
6  error,
7}

Теперь для того чтобы хранить наше состояние создадим класс. Он будет нашим конечным автоматом.

Создадим в нём приватный ValueNotifier, потому что мы не хотим разрешать модифицировать состояние, не используя наш набор переходов. Доступ к состоянию будет read-only.

1class TrafficLightFSM {
2  final ValueNotifier<TrafficLightState> _state = ValueNotifier<TrafficLightState>(TrafficLightState.off);
3}

Так будет выглядеть пример перехода. Для обработки попытки запрещённой смены состояния, будем вызывать метод _triggerFault. Он установит состояние ошибки (по схеме в состояние ошибки можно перейти из любого другого, так что дополнительная проверка тут не нужна).

1void toRed() {
2  if (_state.value == TrafficLightState.off || _state.value == TrafficLightState.yellow) {
3    _state.value = TrafficLightState.red;
4  } else {
5    _triggerFault();
6  }
7}
8
9void _triggerFault() {
10  _state.value = TrafficLightState.error;
11}

Добавим остальные переходы и соединим в один файл. Также добавим main метод для проверки нашей системы.


Управление состоянием превращает хаос в предсказуемую систему: от логики светофора до запутанных UI-сценариев. Оно вводит строгие правила переходов, изолирует бизнес-логику от представления и пресекает «анархию» зависимых переменных.

Модель конечного автомата (FSM) помогает формализовать эти правила, обеспечивая надёжный контроль над каждым этапом работы приложения. Далее мы рассмотрим это на практических примерах во Flutter.

Но какой бы инструмент вы ни выбрали — от setState до BLoC/Redux — помните: главное не хранить данные, а мастерски управлять ими. Только так можно достичь предсказуемости и стабильности для всех: и пользователей, и разработчиков.

В следующем параграфе мы начнём наш разговор о паттерне BLoC: зачем он нужен, какие проблемы решает и как развивался.

Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E
Предыдущий параграф5.8. IoC: библиотека GetIt
Следующий параграф5.10. BLoC: разбор паттерна