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


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


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


image


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


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


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


  1. все выходы потоки
  2. все входы потоки
  3. этот класс должен убирать логику из визуального интерфейса

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


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


Приложение



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


Задачи:


  1. Убрать всю логику из виджетов
  2. В классе BLoC получать только потоки на вход и выдавать только потоки на выход. Например функция, предложенная в комментах к статье.

image


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


Решение:


  1. Убираем всю логику из виджетов. Делаем класс Stateless и перестаем обновлять состояния с помощью setState.
  2. Отображаем показания счетчика в разных местах с помощью встроенного виджета, который сделан специально для отображения данных из потоков — 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,
          ),
        ),
      ),
    );
  }
}

  1. Создаем отдельный класс, где реализуем 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.


Код примера на github


Всем хорошего кодинга!


Дополнение 1


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


  1. Посмотрим как освобождать память при использовании Stateless и Stateful widgets.
  2. Сделаем второй экран и передадим туда состояние при помощи
    2.1 Inherited Widgets
    2.2 Провайдер
    2.3 Singleton
  3. Напишем тесты и посмотрим как они работают с 2.1, 2.2, 2.3

Если есть идеи, что еще проверить, посмотреть — пишите в комменты.

Комментарии (6)


  1. vr19860507
    12.11.2019 06:49

    Что в этом блоке такого что не может провайдер?


    1. awaik Автор
      12.11.2019 06:58

      Разные паттерны для разных случаев, тут я просто переписал простейший пример для понимания того, как полностью вынести логику и правильно его реализовать.
      Если вы имеете в виду вот этот провайдер pub.dev/packages/provider — то этот подход (пакет) зачастую используется вместе с BLoC, когда надо передавать состояние по разным классам (экранам, виджетам). Как я понимаю, этот пакет написан поверх встроенных Inherited widgets и оказался таким удачным, что команда Flutter тоже теперь в нем участвует + рекомендует к использованию.
      IMHO


      1. vr19860507
        12.11.2019 11:26

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


        1. awaik Автор
          12.11.2019 14:19

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


  1. rchese
    12.11.2019 06:49
    +1

    Спасибо за статью.

    Чтобы избежать утечек памяти, необходимо MyHomePage сделать StatefulWidget'ом, чтобы переопределить метод dispose с вызовом соответствующего метода объекта counterBloc.


    1. awaik Автор
      12.11.2019 07:02

      Да, использование StatefulWidget упростит dispose и обычно так и делается. Тут я хотел показать, что при использовании BLoC мы можем полностью уйти от управления состоянием через setState методы.