[Из песочницы] Управление состоянием приложения в Flutter

Привет, Хабр! Представляю перевод статьи, Let me help you to understand and choose a state management solution for your app, которая попалась мне и заинтересовала в процессе изучения азов управления состоянием во Flutter. Буду рад услышать любую критику касательно данного перевода. В обратных кавычках (``) будут написаны мои личные мысли и пояснения.
anbbmzniucp0j9vgce-jkazayam.png
Управление состоянием во Flutter — горячая тема. Возможных вариантов решения задачи много и запутаться в них, выбирая наиболее подходящий под ваши потребности — крайне просто. Я сам путался, но нашел подходящее решение. Позвольте поделиться им с вами.

Чтобы найти решение, подходящее к вашим потребностям, необходимо определить сами потребности. В моем случае это:

  • Иметь возможность развития проекта без ущерба качеству кода
  • Разделить логику отображения от бизнес-логики
  • Иметь понятный код, который сложно поломать
  • Предсказуемость и понятность кода


Учитывая эти требования, подходящими вариантами остаются:

  • Использование метода setState() и Stateful-виджетов
  • `Библиотека` ScopedModel
  • Применение паттерна BLoC (Компоненты бизнес-логики)
  • Redux


Разница между локальным и глобальным состоянием


Перед тем, как погрузиться в анализ отобранных решений, необходимо понять разницу между локальным и глобальным состоянием. Для этого подойдет практический пример:
Представим форму авторизации, где пользователю предлагается ввести логин и пароль и получить объект «личности пользователя» после отправки формы. В этом примере любая проверка данных, вводимых в поля формы — будет являться частью локального состояния `виджета формы авторизации`, и остальная часть приложения не должна знать об этом. А возвращаемый `сервером авторизации` объект «личности» — частью глобального состояния. Так, как от этого объекта зависят другие компоненты, меняющие поведение в зависимости от того, авторизован ли пользователь.

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

j2gwpy580ri_d_fqt0ky0cpmlvs.png

Моя рекомендация — использовать BLoC для управления локальным состоянием и Redux для глобального состояния, особенно, если вы создаете сложное приложение, которое будет расти со временем.



Почему не стоит использовать setState ()


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

ScopedModel — шаг в верном направлении


ScopedModel — библиотека стороннего разработчика Brian Egan. Она дает возможность создавать специальные объекты Models, а также использовать метод notifyListeners() тогда, когда это необходимо. Например, для отслеживания на любого изменения свойства объекта модели:

class CounterModel extends Model {
  int _counter = 0;
  int get counter = _counter;
  void increment() {
    _counter++;
    notifyListeners();
  }
}


В наших виджетах мы сможем реагировать на изменения в модели с помощью `предоставляемого данной библиотекой` виджета ScopedModelDescendant:

class CounterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) { 
    return new ScopedModel(
      model: new CounterModel(),
      child: new Column(children: [
        new ScopedModelDescendant(
          builder: (context, child, model) => new Text('${model.counter}'),
        ),
        new Text("Другой виджет, который не зависит от CounterModel")
      ])
    );
  }
}


В противовес использованию подхода setState(), данное решение позволяет отделить логику отображения от бизнес логики. Однако, оно накладывает определенные ограничения:

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


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

Мощное решение — BLoC


Данный паттерн был придуман в Google и там же его используют. Он поможет нам достичь следующих целей:

  • Разделение логики отображения от бизнес логики
  • Использование асинхронной природы для отображения интерфейса
  • Возможность переиспользования в разных Dart-приложениях, таких, как Flutter или AngularDart


Идея данного подхода очень проста:

  • BLoC использует
    Sink
    
    Api для описания асинхронно поступающих в наши компоненты данных
  • BLoC использует
    Stream
    
    Api для описания асинхронно возвращаемых нашими компонентами данных
  • Наконец, мы можем использовать виджет StreamBuilder для управления потоками данных, без приложения усилий с нашей стороны к подпискам на обновления данных и перерисовку виджетов


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

Я и сам очень рекомендую использовать данный подход для управления локальным состоянием, однако он подойдет даже и для управления глобальным состоянием. Однако в последнем случае вы столкнетесь с проблемой — где и как правильно внедрять BLoC, чтобы к нему имели доступ разные компоненты, и тут на сцену выходит Redux.

Redux и BLoC — идеальный микс для меня


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

Единственный источник истины — все состояние `state` вашего приложения хранится в древовидном объекте в единственном хранилище `store`

  • Состояние доступно только для чтения — единственный способ изменить состояние — это вызвать специальный объект action, описывающий, что должно произойти с состоянием
  • Изменения производятся с помощью чистых функций — для определения того, что изменяется в состоянии вы пишите чистую функцию `reducer`, которая не должна вызывать никаких побочных эффектов `Ссылка на пример кода`


0h43fp5oddvqxfhez1pdbfjau90.png
Ссылка на оригинальный пост, откуда взято изображение

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

Brian Egan разрабатывает как оригинальный Redux, так и flutter_redux, а также он сделал потрясающее Todo приложение, в котором он применил множество архитектурных паттернов, включая Redux.
Учитывая все качества Redux, я очень сильно советую использовать его для управления глобальным состоянием, но вы должны быть уверены, что не используете его для управления локальным состоянием, если хотите масштабировать свое приложение.

Последние слова


В данной статье нет полностью правильного или неправильного решения. Чтобы определиться с тем, какой подход применять в вашем проекте, вам необходимо определиться с вашими потребностями. Для меня и моих целей комбинация Redux и BLoC позволяет моим проектам быстро и безопасно расти, а также облегчает вход сторонних разработчиков в эти проекты, благодаря доступным и понятным инструментам. Однако не у всех одинаковые потребности и с течением времени можно находить как проблемы `в текущих инструментах`, так и еще лучшие решения. Очень важно всегда оставаться любопытным, учиться и думать, подходит ли вам тот или иной инструмент.

© Habrahabr.ru