И еще раз про BLoC на классическом примере счетчика Flutter.
Читая некоторые статьи про реактивное программирование и используя BLoC паттерн в приложениях я понимал, что чего-то не догоняю. Как обычно на все не хватает времени, но вот, выдался свободный час и силы есть — решено, напишу простейшее приложение на Flutter с паттерном BLoC.
Под катом анимашка приложения и пояснения почему я его написал его именно так. Очень интересно мнение сообщества.
Да, про этот паттерн уже много раз писали, но все равно, по нему нет четких инструкций и правил применения и часто возникает вопрос, как правильно реализовать логику в приложении.
Цель статьи внести немного ясности для себя и, надеюсь, для читателей.
Итак, определение паттерна, как его озвучили инженеры Гугл — BLOC это простой класс у которого:
- все выходы потоки
- все входы потоки
- этот класс должен убирать логику из визуального интерфейса
Для реализации данного паттерна мы, по потребности, можем использовать библиотеку rxdart, а можем и не использовать.
Это не уберет реактивность как можно подумать. Как пишут сами создатели пакета — rxdart добавляет функциональности на уже встроенные богатые возможности языка Dart в работе с потоками.
Приложение
Итак, берем приложение счетчиками, которое создается автоматически при создании проекта и пытаемся переписать его с применением паттерна BLoC.
Задачи:
- Убрать всю логику из виджетов
- В классе BLoC получать только потоки на вход и выдавать только потоки на выход. Например функция, предложенная в комментах к статье.
нам не подходит, так как она нарушает правило передачи в класс только потоков.
Решение:
- Убираем всю логику из виджетов. Делаем класс Stateless и перестаем обновлять состояния с помощью setState.
- Отображаем показания счетчика в разных местах с помощью встроенного виджета, который сделан специально для отображения данных из потоков — StreamBuilder.
class MyHomePage extends StatelessWidget {
CounterBloc counterBloc = CounterBloc();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: StreamBuilder<int>(
stream: counterBloc.pressedCount,
builder: (context, snapshot) {
return Text(
'Flutter Counter Bloc Example - ${snapshot.data.toString()}',
);
}),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StreamBuilder<int>(
stream: counterBloc.pressedCount,
builder: (context, snapshot) {
return Text(
'${snapshot.data.toString()}',
style: Theme.of(context).textTheme.display1,
);
}),
],
),
),
floatingActionButton: Container(
width: 100.0,
height: 100.0,
child: FloatingActionButton(
onPressed: () {
counterBloc.incrementCounter.add(null);
},
tooltip: 'Increment',
child: Text(
"+ \n send \n to BLoC",
textAlign: TextAlign.center,
),
),
),
);
}
}
- Создаем отдельный класс, где реализуем BLoC паттерн:
3.1 Все свойства и метода класса, скрыты.
3.2 Для получения и передачи состояния используем потоки, которые видны снаружи при помощи getters (а вот [классная статья про них])(https://habr.com/ru/post/464095/).
class CounterBloc {
int _counter;
CounterBloc() {
_counter = 1;
_actionController.stream.listen(_increaseStream);
}
final _counterStream = BehaviorSubject<int>.seeded(1);
Stream get pressedCount => _counterStream.stream;
Sink get _addValue => _counterStream.sink;
StreamController _actionController = StreamController();
StreamSink get incrementCounter => _actionController.sink;
void _increaseStream(data) {
_counter += 1;
_addValue.add(_counter);
}
void dispose() {
_counterStream.close();
_actionController.close();
}
}
Все, у нас получился рабочий пример, в котором мы передаем и получаем состояние потоками и отображаем данные в нужных виджетах не применяя никакой логики в визуальной части.
Таким образом мы можем отделить все расчеты, передачи данных и т.п. от интерфейса. Например, если добавить к счетчику разные надписи в зависимости от количества и брать их из внешней базы, то вся эта логика уже будет проходить через BLoC — наш интерфейс ничего об этом не будет знать.
Примечание 1: заметьте, что мы создаем экземпляр класса > CounterBloc counterBloc = CounterBloc(); и далее получаем из него данные. Если эти данные нам нужны на разных экранах (в разнесенных классах), то мы можем либо использовать Inherited widgets для передачи или сделать из нашего класса Singleton.
Всем хорошего кодинга!
Дополнение 1
По предложениям сообщества попробую выбрать время и написать продолжение в в котором:
- Посмотрим как освобождать память при использовании Stateless и Stateful widgets.
- Сделаем второй экран и передадим туда состояние при помощи
2.1 Inherited Widgets
2.2 Провайдер
2.3 Singleton - Напишем тесты и посмотрим как они работают с 2.1, 2.2, 2.3
Если есть идеи, что еще проверить, посмотреть — пишите в комменты.
Комментарии (6)
rchese
12.11.2019 06:49+1Спасибо за статью.
Чтобы избежать утечек памяти, необходимо MyHomePage сделать StatefulWidget'ом, чтобы переопределить метод dispose с вызовом соответствующего метода объекта counterBloc.awaik Автор
12.11.2019 07:02Да, использование StatefulWidget упростит dispose и обычно так и делается. Тут я хотел показать, что при использовании BLoC мы можем полностью уйти от управления состоянием через setState методы.
vr19860507
Что в этом блоке такого что не может провайдер?
awaik Автор
Разные паттерны для разных случаев, тут я просто переписал простейший пример для понимания того, как полностью вынести логику и правильно его реализовать.
Если вы имеете в виду вот этот провайдер pub.dev/packages/provider — то этот подход (пакет) зачастую используется вместе с BLoC, когда надо передавать состояние по разным классам (экранам, виджетам). Как я понимаю, этот пакет написан поверх встроенных Inherited widgets и оказался таким удачным, что команда Flutter тоже теперь в нем участвует + рекомендует к использованию.
IMHO
vr19860507
Мой вопрос в том, что они оба нужны для управления состоянием, архитектуру приложения в целом на них не сделаешь. Другими словами, например мы используем клин архитектуру и в слое отвечающем за юай будет либо блок, либо блок через провайдер, либо провайдер. Вариант с чистым провайдером в разы проще и удобнее и писать в разы меньше. Поэтому и спросил что в этом блоке такого? Его везде позиционируют как что то маст хев для крупных приложений, но это совершенно не так.
awaik Автор
Мне кажется, чтобы ответить на этот вопрос, надо взять какую-нибудь типовую проблему и написать 2 варианта решения.
И посмотреть что получается в разных вариантах.
Вы вполне можете быть правы, но вот так сразу я не готов точно сказать, так как очень много разных проблем в реальных приложениях и мне часто не хватало только провайдера.
Но это не показатель конечно.