bloc flutter что это

BLoC паттерн на простом примере

И еще раз про BLoC на классическом примере счетчика Flutter.

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

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

Да, про этот паттерн уже много раз писали, но все равно, по нему нет четких инструкций и правил применения и часто возникает вопрос, как правильно реализовать логику в приложении.

Цель статьи внести немного ясности для себя и, надеюсь, для читателей.

Итак, определение паттерна, как его озвучили инженеры Гугл — BLOC это простой класс у которого:

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

Это не уберет реактивность как можно подумать. Как пишут сами создатели пакета — rxdart добавляет функциональности на уже встроенные богатые возможности языка Dart в работе с потоками.

Приложение

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

Задачи:

нам не подходит, так как она нарушает правило передачи в класс только потоков.

Решение:

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

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

Примечание 1: заметьте, что мы создаем экземпляр класса > CounterBloc counterBloc = CounterBloc(); и далее получаем из него данные. Если эти данные нам нужны на разных экранах (в разнесенных классах), то мы можем либо использовать Inherited widgets для передачи или сделать из нашего класса Singleton.

Источник

Flutter. BlOC, Provider, async – архитектура «по полочкам»

Когда пытаешься написать приложение, то первое с чем сталкиваешься – это как организовать архитектуру приложения. А когда еще при этом речь идет про Flutter, так голова совсем может пойти кругом от того, что выдает Гугл — Vanilla, Scoped Model, BLoC, MVP, MVC, MVVM, MVI и т.д. Предположим вы решили пойти самым модным путем (тем, что советовал Google в 2018 году) и использовать BLOC. Что это? Как этим пользоваться? А может Redux или RxDart? – хотя стоп – это же про «другое» … А все-таки что дальше? Какие библиотеки подключать? Bloc, Flutter_bloc, bloc_pattern и т.д.?

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

Для кого статья

Статья в первую очередь будет полезна тем, кто только начинает осваивать Flutter и не знает с чего начать. Я покажу один из вариантов реализации приложения на Flutter. Это позволит вам «пощупать» Flutter, а дальше уже сами решите, как и с использованием чего будете писать свои приложения.

Паттерны и инструменты. Кратко и просто

И так начнем. Первое что стоит отметить, что есть архитектура приложения (паттерн, шаблон, некая концепция построения) – это как раз-таки: BLoC, MVP, MVC, MVVM, MVI и т.д. Многие из этих архитектур используются не только на Flutter, но и в других языках программирования. Вопрос – что из этого выбрать? На мой взгляд нужно выбрать то, что вы сами хорошо знаете, но только если это подразумевает реактивность и жесткое отделение бизнес логики от интерфейса (да-да – «автомобиль может быть любого цвета, если он черный»). Насчет разделения интерфейса и бизнес-логики, думаю, объяснять не надо, а вот насчет реактивности – попробуйте, если не пробовали – в итоге это действительно очень удобно и «красиво». Если сами выбрать не можете, то давайте пока позволим это сделать за нас не самым глупым парням из Google – BLOC. С архитектурой разобрались.

Теперь инструменты – есть уже готовые библиотеки — Bloc, Flutter_bloc, bloc_pattern – какая лучше? Не знаю – все хороши. Можно долго выбирать и сравнивать, но тут опять как в армии – лучше пока принять не правильное решение, чем не принять никакого. И я пока предлагаю пойти опять в след за модой и использовать Provider (то, что те же самые парни, рекомендуют использовать в 2019 году).

Все это позволит нам сделать как глобальный bloc, так и локальные bloc-и по мере необходимости. Про архитектуру BLoC (именно, паттерн, а не библиотеки) уже много написано, думаю, не стоит подробно снова на ней останавливаться. Отмечу только лишь один момент в данной статье будет использоваться не классический BLoC, а немного модифицированный – в BLoC action (события) будут передаваться не через Sink-и, а будут вызываться функции BLoC-а. Просто, на данный момент, не вижу преимущества использования Sink-ов – а раз их нет, то зачем усложнять себе жизнь?

Асинхронность и параллельные вычисления в Dart

Также стоит немного разъяснить понятие асинхронности в Dart, раз уж мы говорим про реактивность. Очень часто на первых этапах знакомства с Dart, не правильно понимается смысл асинхронных функций (async). Надо всегда помнить, что «по-умолчанию» программа выполняется в одном потоке, а асинхронность лишь позволяет изменить последовательность выполнения команд, а не выполнять их параллельно. То есть если просто запустить функцию с большими вычислениями всего лишь пометив ее async, то интерфейс заблокируется. Async НЕ запускает новый поток. Как работает async и await есть много информации в интернете, поэтому останавливаться на этом тоже не буду.

Читайте также:  beatmap difficulty calculation is running in the background что это

Если же надо произвести какие-то большие вычисления и при этом не заблокировать интерфейс, то надо использовать функцию compute (для особого хардкора можно воспользоваться isolate-ами). Это действительно запустит отдельный поток выполнения, у которого будет также своя отдельная область памяти (что очень грустно и печально). Общаться с такими потоками можно, только через сообщения, которые могут содержать простые типы данных, их списки.

Приступим к практике

Постановка задачи

Давайте попробуем написать простейшее приложение – пусть это будет некий телефонный справочник. В качестве хранилища будем использовать Firebase – это позволит сделать «облачное» приложение. Как подключить Firebase к проекту я пропущу (на эту тему написана не одна статья и повторяться не вижу смысла. Замечание: в данном проекте используется Cloud Firestore.).

Должно получиться так:

Описание приложения

Наше приложение внешне будет содержать:

Функционировать приложение будет так – при открытии экрана соответствующий bloc инициализируется начальным значением состояния (state) и при необходимости в конструкторе bloc вызывается некоторый начальный action. Экран строится/перестраивается на основании state, которое возвращает bloc. Пользователь совершает некоторые действия в приложении, которые имеют соответствующие action-ы. Action-ы передаются в класс bloc, там в функции mapEventToState они обрабатываются и bloc возвращает новое state обратно в экран, на основании которого экран перестраивается.

Структура файлов

Первым делом создаем пустой проект Flutter и сделаем структуру проекта такого вида (отмечу, что в демо-проекте некоторые файлы в итоге останутся пустыми):

Окно авторизации. MainBloc

Теперь необходимо реализовать авторизацию в Firebase.
Начнем с создания классов событий (через события в bloc удобно передавать данные) и состояний для Main bloc-а:

Флаг busy в классе состояния используется для вывода в интерфейсе progress_hud и исключения лишних считываний данных из базы при скролле списка. Перед началом всех операций в блоке в выходной поток выдается новое состояние старого типа с установленным флагом busy – таким образом интерфейс получает уведомление о том, что операция началась. По окончанию операции в поток подается новое состояние со сброшенным флагом busy.

Наследники класса MainBlocState описывают состояния основного Bloc-а приложения. Наследники MainBlocAction описывают события, возникающие в нем.

Класс MainBloc содержит 4 основных элемента – функцию «преобразования» событий в состояния (Future mapEventToState), состояние Bloc-а — _blocState, репозитарий bloc-а — repo и «выходной» поток состояний (который отслеживают элементы интерфейса) – blocStream. В принципе, это все элементы, обеспечивающие функциональность bloc-a. Иногда целесообразно использовать 2 выходных потока в одном bloc-е – такой пример будет ниже. Приводить его листинг здесь не буду – можно посмотреть, скачав проект.

Класс репозитория bloc-а содержит логику работы с Firebase и объект(data) хранящий данные, необходимые для бизнес логики, которую реализует данный bloc.

В классе MainData тоже хранится состояние, но состояние авторизации в Firebase, а не состояние Bloc.

Логику для основного bloc-а написали, теперь можно приступить к реализации экрана авторизации/регистрации.

MainBloc инициализируется в файле main:

Самое время сделать небольшое отступление про StreamBuilder, Provider, StreamProvider, Consumer и Selector.

Отступление про Provider-ы

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

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

StreamProvider — виджет, который следит за потоком и при получении нового объекта, сигнализирует, что дочерние (те, которые объявлены отдельным классом с методом build) виджеты должны перестроиться.

Consumer и Selector являются «синтаксическим сахаром», т.е. это фактически «обертка», которая содержит build и прячет под собой виджет. В Selector-e можно сделать дополнительную фильтрацию обновлений.

Таким образом, когда при каждом событии надо перестроить большую часть экрана, то можно использовать вариант с Provider и StreamBuilder. Когда же надо перестроить части дерева виджетов близкие к листьям, то для исключения лишних перестроений дерева целесообразно использовать StreamProvider в сочетании с Consumer и Selector.

Авторизация. Продолжение

При входе в приложение пользователь должен попасть на окно авторизации/регистрации, и в этот момент ему еще не должно быть доступно меню приложения. Второй момент – данный экран обновлять частично не имеет особого смысла, поэтому для построения интерфейса мы можем использовать StreamBuilder. И третий момент в проекте используется Navigator для навигации между экранами. При получении события успешной авторизации необходимо вызвать переход на экран информации. Но просто внутри build StreamBuilder-а такое не получится – будет ошибка. Чтобы это обойти можно воспользоваться вспомогательным классом-оберткой StreamBuilderWithListener (Eugene Brusov — stackoverflow.com).

Теперь сам листинг данного экрана auth_screen(приведу тут частично):

В первую очередь создается StreamBuilderWithListener для прослушивания потока из bloc-а. И на основе текущего состояния вызывается либо виджет LoggedWidget (если пользователь уже авторизован), либо SignInAndSignUpWidget (если пользователь не авторизован еще). В случае если bloc возвращает состояние IsLogged переключение на новый экран посредством Navigator происходит не в builder (что привело бы к ошибке), а в listener. В нижележащих виджетах происходит построение интерфейса на основе данных, возвращаемых здесь. Тут фактически используется связка Provider+ StreamBuilder, т.к. при изменении состояния блока фактически весь интерфейс меняется.

Для передачи данных в bloc используются TextEditingController и параметры action-ов:

Окно PhoneBookScreen

И теперь давайте немного поговорим о своем окне PhoneBookScreen. Это самое интересное окно – тут интерфейс строится на основе 2-х потоков из bloc-а, а еще тут есть список со скроллом и пагинацией (пэйджинацией, pagination).

Первый StreamProvider нужен для переключения между различными экранами справочника – список, карточка контакта, карточка контакта для редактирования и т.п. Выбор виджета для экрана происходит в функции caseWidget (но в этом примере реализован только вид для списка – можете попробовать реализовать вид для карточки контакта – это очень просто и будет не плохим началом.).

Читайте также:  какой краской расписывают стены в храме

На этом экране уже используется связка StreamProvider + Selector/Consumer, т.к. тут есть скролл списка и при нем перестраивать весь экран не целесообразно (т.е. перестроение виджетов происходит от соответствующего Selector/Consumer и ниже по дереву).

И вот реализация самого списка:

Тут видим второй StreamProvider, который следит за вторым потоком bloc-а, который отвечает за скролл. Пагинация организована стандартно через _scrollListener (controller: _scrollController). Хоть окно и интересное, но с учетом подробного описания первого окна — больше ничего нового тут сказать нечего. Поэтому на этом сегодня — все.

Задачей данной статьи не было показать идеальный код, то есть тут можно найти многие моменты для оптимизации — правильно «разбить» по файлам, использовать где-то instance, mixin-ы и тому подобное. Также, что «напрашивается» следующим этапом – можно сделать карточку контакта. Основная задача была структурировать знания, задать некоторый вектор для конструирования приложения, дать разъяснения по некоторым не очень очевидным на первых этапах знакомства моментах проектирования приложения на Flutter.

Проект можно скачать по адресу (для регистрации можно использовать любую почту, с паролем не менее 6 символов. При повторной авторизации пароль должен совпадать с тем, что использовался при регистрации).

Источник

Flutter BloC паттерн + Provider + тесты + запоминаем состояние

Эта статья выросла из публикации “BLoC паттерн на простом примере” где мы разобрались, что это за паттерн и как его применить в классическом простом примере счетчика.

По комментам и для своего лучшего понимания я решил попробовать написать приложение в котором будут получены ответы на вопросы:

Ниже анимашка получившегося примера, а под катом разбор полетов 🙂

И ещё в конце статьи интересная задачка — как модифицировать приложение для применения Debounce оператора из ReactiveX паттерна (если точнее, то reactiveX — расширение Observer pattern)

Описание приложения и базового кода

Не имеет отношения к BLoC и Provider

Связано с BLoC и Provider

Пишем приложение

Как следует из определения паттерна BLoC наша задача убрать из виджетов всю логику и работать с данными через класс в котором все входы и выходы — Streams.

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

Для этого есть разные методы, а именно:

В данном примере мы будем использовать Provider — привести пример всех методов не хватило сил 🙂

Общая структура

Итак, у нас есть класс

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

После добавления этой красивой конструкции в любом виджете внизу дерева нам доступен объект со всеми данными. Подробно как работать с Provider тут и тут.

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

Класс для BLoC

Для этого мы создаем класс BLoC, в котором описываем не только потоки, но и получение и запись состояния из постоянного хранилища телефона.

Если мы внимательно посмотрим на этот класс, то увидим, что:

Маленькие задачки для лучшего понимания:

Получаем и передаем данные в приложении

Теперь нам надо передать данные в Stream при нажатии кнопочек или свайпе и получить эти данные на карточке и на отдельном экране.

Есть разные варианты как это сделать, я выбрал классический, мы оборачиваем те части дерева, где нужно получать \ передавать данные в Consumer

Ну и далее получение данных
_swipesBloc.pressedCount,

Передача данных
_swipesBloc.incrementCounter.add(1);

Вот и все, мы получили понятный и расширяемый код в правилах BLoC паттерна.

Тесты

Тестировать можно виджеты, можно делать моки, можно e2e.

Мы протестим виджеты и запустим приложение с проверкой как сработало увеличение счетчика. Информация по тестам тут и тут.

Тестирование виджетов

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

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

Для запуска теста используем команду
flutter test

Integration tests (Интеграционные тесты)

В этом варианте теста приложение запускается на эмуляторе, мы можем нажимать кнопочки, свайпать и проверять что получилось в результате.

Для этого мы создаем 2 файла:

В первом подключаем что нужно, а во втором непосредственно тесты. Для примера я сделал проверки:

Задача

Просто для углубления понимания. В современных приложениях (сайтах) часто используется функция Debounce из ReactiveX.

Например:

Задача: сделать чтобы цифра менялась только если между нажатиями на + или — прошло более 2 секунд. Для этого править только BLoC класс, весь остальной код должен остаться тем же самым.

Вот и все. Если что-то криво или неправильно, поправляйте тут или на github, попробуем достичь идеала 🙂

Источник

Основы архитектуры приложений на Flutter: Vanilla, Scoped Model, BLoC

(оригинал статьи на английском языке опубликован на Medium)

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

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

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

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

Изначально пользователю показывается экран с кнопкой “Load user data” расположенной по центру. Когда пользователь нажимает на кнопку, происходит асинхронная загрузка данных, и кнопка заменяется индикатором загрузки. Когда загрузка данных завершена, индикатор загрузки заменяется данными.

Данные

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

Vanilla

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

Открываем экран VanillaScreen с помощью Navigator

Для того, чтобы отобразить индикатор прогресса, когда пользователь нажимает кнопку “Load user details” мы делаем следующее.

Из документации (перевод):

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

Плюсы

Минусы

Scoped Model

Scoped Model это сторонняя библиотека. Вот как разработчики ее описывают:

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

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

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

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

Плюсы

Минусы

BLoC (Business Logic Components) это паттерн, рекомендованный разработчиками из компании Google. Для управления состоянием и для уведомления об изменении состояния используются потоки.

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

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

UserInitState для состояния, когда пользователь открывает экран с кнопкой в центре.

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

UserDataState для состояния, когда данные уже загружены и показаны на экране.

Плюсы

Минусы

Линки

Мы можете ознакомиться с полным кодом, скачав его с моего репозитория на github.

Источник

Архитектурный паттерн BLoC в проекте: используем легкий Cubit

Привет! Меня зовут Юрий Петров, я Flutter-разработчик в Friflex и автор канала о мобильной разработке Мобильный разработчик. В этой статье я хотел бы обсудить библиотеку flutter_bloc и одну из ее реализаций Cubit.

Для начала, необходимо понять, что такое BLoC. Это архитектурный паттерн, акроним от Business Logic Component («компонент бизнес-логики»). C помощью этого паттерна мы можем легко отделить бизнес-логику приложения от пользовательского интерфейса.

Библиотека flutter_bloc и реализует этот паттерн и является менеджером состояния приложения. С помощью этой библиотеки мы можем:

У библиотеки flutter_bloc есть две реализации: Bloc и Cubit.

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

Cubit же немного отличается тем, что events отсутствует, и напрямую мы обращаемся в Сubit, который в свою очередь генерирует (emit) новое состояние.

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

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

Как это можно реализовать? Для начала нужно понять, что такое AppState. AppState – это глобальное состояние нашего приложения. В целом, состояний может быть два – авторизован пользователь в приложении или нет. От этого состояния зависит многое, например, как будут вести себя другие блоки, какие виджеты будут нарисованы на экране и так далее. На примере приложения AliExpress, видно как меняется интерфейс при изменении состояния приложения «Авторизован/Не авторизован».

Для решения задачи регистрации будем использовать три Cubit: AuthCubit, RegistrationCubit, UserCubit.

Согласно этой схеме, мы видим, что при успешной регистрации RegistrationCubit получает АuthData, в которой хранится apiToken. Эти данные мы передаем в AuthCubit, который меняет своё состояние на «Авторизован». Далее мы видим, что у нас есть UserCubit, который подписан с помощью Stream Subscription на AuthCubit. И в случае изменения состояния в AuthCubit, UserCubit обращается к репозиторию и получает данные о пользователе.

И сразу встает вопрос: как пересоздавать интерфейс при изменении состояния AuthCubit? Есть несколько решений. Первое и самое простое – в методе build() мы будем проверять с помощью условного оператора If, в каком состоянии AuthCubit. Но есть и более гибкое решение.

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

Источник

Сказочный портал