Flutter предоставляет современный фреймворк в стиле реакции, богатую коллекцию виджетов и инструменты, но нет ничего похожего на руководство по архитектуре приложений Android.

Действительно, не существует окончательной архитектуры, которая бы отвечала всем возможным требованиям, но давайте признаем тот факт, что большинство мобильных приложений, над которыми мы работаем, имеют по крайней мере некоторые из следующих функций:

  1. Запрос / загрузка данных из / в сеть.
  2. Сопоставьте, преобразуйте, подготовьте данные и представьте их пользователю.
  3. Помещать / получать данные в / из базы данных.

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

Пользователь представлен с кнопкой «Загрузить данные пользователя» в центре экрана. Когда пользователь нажимает кнопку, запускается асинхронная загрузка данных, и кнопка заменяется индикатором загрузки. После загрузки данных индикатор загрузки заменяется данными.

Давайте начнем.

Данные

Для простоты я создал класс Repository, содержащий метод getUser(), который имитирует асинхронный сетевой вызов и возвращает объект Future<User> с жестко заданными значениями.

Если вы не знакомы с Futures и асинхронным программированием в Dart, вы можете узнать об этом больше, следуя этому руководству и читая doc.

Ваниль

Давайте создадим приложение так, как это сделали бы разработчики после прочтения официальной документации Flutter.

Переход к VanillaScreen экрану с помощью Navigator

Поскольку состояние виджета может меняться несколько раз за время его существования, мы должны расширить StatefulWidget. Для реализации виджета с отслеживанием состояния также требуется класс State. Поля bool _isLoading и User _user в классе _VanillaScreenState представляют состояние виджета. Оба поля инициализируются перед вызовом метода build(BuildContext context).

Когда создается объект состояния виджета, вызывается метод build(BuildContext context) для построения пользовательского интерфейса. Все решения о виджетах, которые должны быть построены для представления текущего состояния, принимаются в коде объявления пользовательского интерфейса.

body: SafeArea(
child: _isLoading ? _buildLoading() : _buildBody(),
)

Чтобы отобразить индикатор прогресса, когда пользователь нажимает кнопку «Загрузить сведения о пользователе», мы делаем следующее.

setState(() {
_isLoading = true;
});

Вызов setState() уведомляет платформу о том, что внутреннее состояние этого объекта изменилось таким образом, что это может повлиять на пользовательский интерфейс в этом поддереве, что заставляет платформу планировать сборку для этого объекта State.

Это означает, что после вызова метода setState() фреймворком снова вызывается метод build(BuildContext context) и все дерево виджетов перестраивается. Поскольку для _isLoading теперь установлено значение true, метод _buildLoading() вызывается вместо _buildBody() и индикатора загрузки отображается на экране. То же самое происходит, когда мы обрабатываем обратный вызов от getUser() и вызываем setState() для переназначения полей _isLoading и _user.

widget._repository.getUser().then((user) {
setState(() {
_user = user;
_isLoading = false;
});
});

Плюсы

  1. Легко учиться и понимать.
  2. Никаких сторонних библиотек не требуется.

Минусы

  1. Все дерево виджетов перестраивается каждый раз при изменении состояния виджета.
  2. Это нарушение принципа единой ответственности. Виджет отвечает не только за создание пользовательского интерфейса, но и за загрузку данных, бизнес-логику и управление состоянием.
  3. Решения о том, как должно быть представлено текущее состояние, принимаются в коде объявления пользовательского интерфейса. Если бы у нас был более сложный код состояния, читаемость снизилась бы.

Модель в области видимости

Scoped Model - это сторонний пакет, не входящий во фреймворк Flutter. Вот как это описывают разработчики Scoped Model:

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

Давайте создадим тот же экран, используя Scoped Model. Во-первых, нам нужно установить пакет Scoped Model, добавив scoped_model зависимость к pubspec.yaml в разделе dependencies.

scoped_model: ^1.0.1

Давайте взглянем на виджет UserModelScreen и сравним его с предыдущим примером, который был создан без использования Scoped Model. Поскольку мы хотим сделать нашу модель доступной для всех потомков виджета, мы должны обернуть ее общим ScopedModel и предоставить виджет и модель.

В предыдущем примере все дерево виджетов было перестроено при изменении состояния виджета. Но нужно ли перестраивать весь экран? Например, AppBar вообще не должен меняться, поэтому нет смысла его перестраивать. В идеале мы должны перестраивать только те виджеты, которые обновляются. Scoped Model может помочь нам решить эту проблему.

ScopedModelDescendant<UserModel> виджет используется для поиска UserModel в дереве виджетов. Он будет автоматически перестраиваться всякий раз, когда UserModel уведомляет о том, что изменение произошло.

Еще одно улучшение состоит в том, что UserModelScreen больше не отвечает за управление состоянием и бизнес-логику.

Давайте посмотрим на UserModel код.

Теперь UserModel держит и управляет государством. Чтобы уведомить слушателей (и перестроить потомков) о том, что изменение произошло, необходимо вызвать метод notifyListeners().

Плюсы

  1. Бизнес-логика, управление состоянием и разделение кода пользовательского интерфейса.
  2. Легко учить.

Минусы

  1. Требуется сторонняя библиотека.
  2. По мере того, как модель становится все более сложной, становится все труднее отслеживать, когда следует вызывать notifyListeners().

BLoC

BLoC (B usiness Lo gic Компоненты) - это шаблон, рекомендованный разработчиками Google. Он использует функции потоков для управления изменениями состояния и их распространения.

Для разработчиков Android: объект Bloc можно рассматривать как ViewModel, а StreamController как LiveData. Это сделает следующий код очень простым, поскольку вы уже знакомы с концепциями.

Для уведомления подписчиков об изменении состояния не требуются дополнительные вызовы методов.

Я создал 3 класса для представления возможных состояний экрана:

  1. UserInitState для состояния, когда пользователь открывает экран с кнопкой в ​​центре.
  2. UserLoadingState для состояния, когда индикатор загрузки отображается во время загрузки данных.
  3. UserDataState для состояния, когда данные загружаются и отображаются на экране.

Распространение изменений состояния таким образом позволяет нам избавиться от всей логики в коде объявления пользовательского интерфейса. В примере с Scoped Model мы все еще проверяли, является ли _isLoading true в коде объявления пользовательского интерфейса, чтобы решить, какой виджет мы должны визуализировать. В случае с BLoC мы распространяем состояние экрана, и единственная ответственность UserBlocScreen виджета - визуализировать пользовательский интерфейс для этого состояния.

UserBlocScreen код стал еще проще по сравнению с предыдущими примерами. Для прослушивания изменений состояния мы используем StreamBuilder. StreamBuilder - это StatefulWidget, который строится на основе последнего снимка взаимодействия с Stream.

Плюсы

  1. Никаких сторонних библиотек не требуется.
  2. Бизнес-логика, управление состоянием и разделение логики пользовательского интерфейса.
  3. Это реактивный. Никаких дополнительных вызовов не требуется, как в случае с notifyListeners() Scoped Model.

Минусы

  1. Требуется опыт работы с потоками или rxdart.

Исходный код

Вы можете ознакомиться с исходным кодом примеров, приведенных выше, в этом репозитории на github.