
Приложение с исходным кодом на 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 дерево виджетов.
Как можно увидеть, для 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().
Комментарии к статье: Пример приложения на Flutter с Provider, Bloc, Cubit и Freezed.