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

    И еще раз про 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 (а вот классная статья про них).

    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


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


    Продолжение


    В продолжении передаем состояние по разным экранам, сохраняем данные в постоянной памяти телефона, тестируем BLoC https://habr.com/ru/post/485002/

    Средняя зарплата в IT

    120 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 7 078 анкет, за 1-ое пол. 2021 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 23

      0
      Что в этом блоке такого что не может провайдер?
        0
        Разные паттерны для разных случаев, тут я просто переписал простейший пример для понимания того, как полностью вынести логику и правильно его реализовать.
        Если вы имеете в виду вот этот провайдер pub.dev/packages/provider — то этот подход (пакет) зачастую используется вместе с BLoC, когда надо передавать состояние по разным классам (экранам, виджетам). Как я понимаю, этот пакет написан поверх встроенных Inherited widgets и оказался таким удачным, что команда Flutter тоже теперь в нем участвует + рекомендует к использованию.
        IMHO
          0
          Мой вопрос в том, что они оба нужны для управления состоянием, архитектуру приложения в целом на них не сделаешь. Другими словами, например мы используем клин архитектуру и в слое отвечающем за юай будет либо блок, либо блок через провайдер, либо провайдер. Вариант с чистым провайдером в разы проще и удобнее и писать в разы меньше. Поэтому и спросил что в этом блоке такого? Его везде позиционируют как что то маст хев для крупных приложений, но это совершенно не так.
            0
            Мне кажется, чтобы ответить на этот вопрос, надо взять какую-нибудь типовую проблему и написать 2 варианта решения.
            И посмотреть что получается в разных вариантах.
            Вы вполне можете быть правы, но вот так сразу я не готов точно сказать, так как очень много разных проблем в реальных приложениях и мне часто не хватало только провайдера.
            Но это не показатель конечно.
              0
              Вот недавно столкнулся — весь код написанный на BLoC без правок переносится в web + «настольные» приложения, так как не привязан к виджетам.
              С provider так не получится, придется переписывать логику.
              Так что мой личный выбор BLoC + провайдер для мутаций по дереву виджетов, без логики.
                0
                awaik, если у вас логика каким-то образом оказалась связана с виджетами, то возможно, вы неверно используете провайдер.
                Provider/ChangeNotifierProvider находятся в дереве виджетов, но классы, которые они создают и провайдят, висят отдельно в воздухе, ничего не знают про контекст и про UI, и занимаются только бизнес-логикой.
                  0
                  Если интересно, давайте я вспомню проблему, на которой это возникало и мы можем обсудить в личке варианты (примерчики накидать), а тут резюме.
                  На сегодняшний день, на пятом приложении, у меня выработалась вообще какая-то своя архитектура, которую тоже интересно было бы обсудить :)
                +1
                vr19860507, я еще покопал эту тему. Объективное отличие одно:
                — Provider+ChangeNotifier и MobX+Provider — это мутабельные архитектуры (с изменением хранилища). Преимущество — как вы справедливо заметили, гораздо меньше писанины.
                — Redux (и его вариации), BLoC Library — иммутабельные архитектуры (выкидывают тебе стейты). Преимущество — ты можешь иметь историю стейтов приложения, «путешествовать во времени» (возвращаться на предыдущие стейты), можешь сохранить целиком состояние приложения. Другой вопрос, насколько это надо.
                P.S. Есть еще плагин StateNotifier (от того же Реми, создателя провайдера), позволяет взять лучшее из обоих подходов, и создавать классы под провайдер с иммутабельными стейтами. Пока в версии 0.5, возможно, будет ещё значительно допиливаться, но штука перспективная.
                  0
                  Вот это интересно, спасибо, как раз выше написал про то, что микс у меня получился архитектур. Попробую как там что.
              0
              vr19860507 моё имхо, что не более чем вкусовщина.
              В провайдер прекрасно можно выносить бизнес-логику.
              Смотрел BLoC и MobX — те же яйца, только в профиль. Но в provider всё прозрачнее и значительно меньше кода.
              Плюс к тому BLoC был чуть ли не единственным решением до провайдера, думаю, что многие его выучили и теперь не могут/не хотят переучиваться.
              Общался в чате с одним чуваком, который всех поучает, везде топит за блок и утверждает, что «провайдер — всего лишь примитивная тулза для доступа по ссылке к объекту, а Блок — это Архитектура». Из дальнейших разговоров выяснилось, что чувак тупо не разбирается, как работает ChangeNotifier и не понимает как организовать обмен данными с провайдером, и понимать не хочет.
              Кстати, подумываю тут сам написать статью про архитектуру на провайдерах на простом примере и просветить народ, а то в официальной документации он объяснен очень поверхностно, чисто синтаксис, без примеров использования.
              +1
              Спасибо за статью.

              Чтобы избежать утечек памяти, необходимо MyHomePage сделать StatefulWidget'ом, чтобы переопределить метод dispose с вызовом соответствующего метода объекта counterBloc.
                0
                Да, использование StatefulWidget упростит dispose и обычно так и делается. Тут я хотел показать, что при использовании BLoC мы можем полностью уйти от управления состоянием через setState методы.
                +1
                Спасибо. Продолжения ждать?
                  0
                  Спасибо :)
                  Да, в течение пары недель, сейчас в проду проект на флаттере выпускаем и сил ни на что не остается.
                    0
                    Гуд, буду ждать. Пока для себя, по тому что узнал, блок вижу более предпочтительным (по крайней мере в сравнении с редаксом), интересно почитать, посмотреть примеры. Единственное — не понимаю в чем практический смысл не только слушать блок через стримы, но и оповещать его о событиях таким же образом.
                      0
                      Потому что реактивность
                        0
                        И? Практический смысл того что из блока данные выходят через стримы я понимаю, можно стрим билдеры использовать что довольно удобно, да и блоку не нужно на вью ссылаться для передачи данных. Но в чем смысл использовать стримы на вход в блок? Я не вижу практических преимуществ перед тем чтобы просто дергать методы блока напрямую.
                          0
                          Если вы программируете в реактивном стиле, то смысл правила — все входы == Streams, становится понятным.

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

                          А если через Streams, то все будет очень красиво и логично.

                          Сам оператор вот тут reactivex.io/documentation/operators/debounce.html

                          (я очень надеюсь скоро опубликовать package для instagram подобных сториз и там как раз все это становится очень понятно)
                            0
                            Хм, действительно, выглядит применение вполне логично, спасибо.
                              0

                              Не очень понимаю, почему код будет кривоватым?


                              debounce / throttle декораторы для функций известны ещё со времён underscore, если не раньше

                                0
                                Да, я согласен. Тут речь все таки про блок паттерн и программирование в реактивном стиле.
                                Если интересно, можем сделать простой пример и посмотреть как будет выглядеть код в разных стилях.
                    +1
                    Продолжение тут habr.com/ru/post/485002
                      0
                      Такое ощущение, что кто-то открыл для себя react + state manager или даже angular

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

                      Самое читаемое