Пример приложения на Flutter с Provider, Bloc, Cubit и Freezed.

Пример приложения на Flutter с Provider, Bloc, Cubit и Freezed.

Приложение с исходным кодом на flutter. Демонстрация использования Freezed, BLoC, Cubit и Provider на примере клиентского приложения для сайта.

Очень часто возникает вопрос как работает BLoC. Из примеров легко найти только Counter. Поэтому я решил набросать пример небольшого приложения, в котором постарался продемонстрировать когда нужен Provider и когда использовать Cubit вместо BLoC. А так же как уменьшить написание кода с помощью Freezed.

Приложение простейший клиент для этого сайта. Приложение должно получать список статей и отображать выбранную.

APK можно скачать тут.
Исходный код посмотреть тут.

Описание под видео.


Архитектура Flutter приложения с BLoC и Provider.

Provider во Flutter.

Я уже писал про библиотеку Provider. С ее помощью можно передавать данные по контексту и получить к ним доступ с затратами на поиск O(1). Данные желательно располагать как можно локализованней. То есть, как можно ближе к нужным виджетам. Если какие то данные нужны только в одном виджете, провайдер не нужен. Так же не стоит располагать данные в провайдере в самом начале дерева, если они нужны где то глубоко в одной ветке.

Repository во Flutter.

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

Это может быть так:

Проверяем есть ли нужные данные локально в телефоне (например в hive, sqflite и т.д.).
Если есть - отдает их, попутно проверяя актуальность на сервере.
Если данных нет, шлем запрос на сервер и информируем пользователя о том, что идет процесс загрузки. Например с помощью индикатора загрузки.
Получив данные - отображаем их.
Получив ошибку - выводим информация о ошибке. Также можно выводить например кнопку "Повторить".

В приложении я не буду использовать СУБД. Я сохраню данные для простоты просто в массив в ОЗУ.

Реализация

Первым делом я создал виджет MyApp. В нем просто MaterialApp. Все что нужно глобально я положил для удобства в отдельный виджет AppInjection.

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

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

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

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

Класс MyBlocObserver можно использовать для отладки и логирования событий, изменения состояний и ошибок всех классов унаследованных от Bloc/Cubit.

Для демонстрации вложенного навигатора я решил набросать класс NetworkScope. Он просто содержит в себе навигатор, и слушает Cubit состояния подключения к сети посылаемое Connectivity.

Я уже писал об отличии блока от Cubit. Решил показать пример использования Cubit и в данном приложении.

Cubit NetworkState возвращает true если соединение есть и false в противном случае. В конструкторе проверяем текущий стейт Connectivity и сообщаем его, а так же подписываемся на изменения.

debounceTime - функция из пакета rxdart. Она ждет заданное время, и если с последнего события прошло это время, то она выдает последний результат. Это нужно чтобы не было ненужных срабатывания в момент переключения сетей. Connectivity в момент переключения сетей выдает кратковременно none.

В виджете NetworkScope я решил продемонстрировать передачу уже созданного объекта с помощью Provider.value().

Я уже описывал варианты использования Provider. Через value() передается вниз по контексту указатель на объект созданный вне провайдера.

То есть NetworkState был создан в строке

final NetworkState _networkState = NetworkState();

И ссылка на него передана в Provider.value().

Все виджеты созданные в этом контексте. То есть в данном случае во вложенном навигаторе, будут иметь доступ к NetworkState из этого виджета.

Провайдер помещен в Stack. Ниже в BlocBuilder нужно передать в параметр bloc наш созданный Cubit.

Сам BlocBuilder не найдет данного объекта в текущем контексте, так как он создан в контексте NetworkScope, а провайдер передает только своим потомкам, то есть в Navigator и глубже.

Можно посмотреть в FlutterInspector дерево виджетов.

flutter inspector widget provider tree

Как можно увидеть, для BlocBuilder родителем является Stack. И нужный провайдер не находится выше по дереву в числе предков.

WillPopScope нужен для отлавливания системной кнопки back в Andoroid. Первый навигатор из MaterialApp содержит только одну страницу. Это NetworkScope. Для него не будет вызываться метод push и поэтому кнопка back сразу свернет приложение, не зависимо от того, на каком на самом деле экране мы находимся. WillPopScope перехватит нажатие кнопки back и если вернет false, то предотвратит обработку кнопки back родительским навигатором.

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

По умолчанию в навигаторе открыта страница ArticleListViewPage. Я разбил страницу на три части.

Первая часть StatelessWidget который в провайдере создает ArticleListBLoC. Вложенные два виджета будут иметь доступ к данному объекту.

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

Плюс в использовании BlocProvider в том, что он автоматически вызовет close для блока, когда будет вызван метод dispose у провайдера.

Если же вы обернете не в BlocProvider а в просто Provider() тогда вам нужно позаботиться о закрытии блока передав соответствующий калбек в параметре dispose виджета Provider

dispose: (BuildContext context, ArticleListBLoC bloc) => bloc.close()

По умолчанию у провайдера ленивая загрузка. То есть как только будет обращение к ArticleListBLoC, тогда и будет создан экземпляр данного объекта. Сразу при создании вызываем событие load, для загрузки списка статей.

В комментариях к коду в ArticleListBLoC описано что происходит.

То есть устанавливаем стейт LoadingArticleListState. На основании этого стейта выводит Loader() (индикатор загрузки).

Если данные из репозитория получены, то возвращаем стейт LoadedArticleListState с данными и выводит список статей. Если произошла ошибка, выводим ошибку и кнопку "Повторить".

В виджете ошибки подписываемся на NetworkState Cubit. Если подключение есть, кнопку отображаем. Если подключения нет, это бессмысленно в нашем случае, и мы ее скрываем.

Далее, при прокрутке списка, смотрим. Если оставшаяся часть более 500 единиц, то вызываем loadMore для подгрузки следующих страниц.

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

При вызове loadMore я не возвращаю стейт loading. Тут опять же есть разные варианты. Возможно вам нужно вернуть стейт loading со списком уже загруженных статей, для отображения результата. А может понадобится отдельный стейт. В данном примере загрузка происходит фоново, и новые итемы добавляются в конец.

Список реализован с помощью вспомогательного класса DataList.

@immutable
class DataList<T> extends Iterable<T> {
  DataList.fromIterable(Iterable<T> list):
        _source = UnmodifiableListView<T>(list.toSet().toList()
          ..sort());

  final UnmodifiableListView<T> _source;

  @override
  Iterator<T> get iterator => _source.iterator;

  @override
  String toString() => 'DataList([${_source.length}])';
}

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

Для нужной сортировки определим в модели, передаваемой в список, метод сравнения. Я сравниваю по ID. Чем больше ID тем новее запись и тем она выше в списке.

  @override
  int compareTo(Article other) {
    return other.id.compareTo(id);
  }

Для отображения экрана с текстом статьи, я создал отдельный блок. Он нужен как и в первом случае на одном экране. Но в отличии от первого варианта, я не стал использовать BlocProvider

Для отображения нужна модель только в одном виджете, я передал ее в конструкторе.

Можно было продемонстрировать еще один вариант, передачу самой модели вниз по дереву, но я уже писал не одну статью на эту тему.

Так как блок основан на стримах, не забываем вызывать метод close() в dispose().


При копировании материалов ссылка на https://terraideas.ru/ обязательна

Комментарии к статье: Пример приложения на Flutter с Provider, Bloc, Cubit и Freezed.

Нет ни одного комментария. Будьте первым!