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


Ниже буквально в 50 строк я на известном примере покажу, что реактивность


а) это не про оффлайн/онлайн
б) это очень просто
в) очень хороша для упрощения практически любого кода


Моим поспешным критикам,
которые без оглядки бросаются в бой, считая что BlocProvider — это provider, рекомендую для общего развития почитать сперва базовую статью, ссылка на которую есть на странице либы flutter_bloc, в первой же строке описания.


Всем известный пример "Счетчик", который генерится при создании Flutter проекта, является мальчиком для битья хорошей стартовой точкой для демонстрации множества практик.
Итак, он содержит MyHomePage extends StatefulWidget, метод _incrementCounter для команды инкремента и setState для перерисовки всей иерархии виджетов.


Внесем реактивность при помощи библиотеки rxdart и нескольких несложных шагов:


Добавим библиотеку в pubspec.yaml


dependencies:
...
  rxdart: 0.22.2

Изменим архитектуру счетчика и добавим event


class _Counter {
  int _count;

  int get count => _count;

  _Counter(this._count)
      : this.onCounterUpd = BehaviorSubject<int>.seeded(_count);

  /// Создадим евент.
  final BehaviorSubject<int> onCounterUpd;

  /// Вынесем инкремент за пределы виджета, добавим генерацию события.
  Future incrementCounter() async {
    onCounterUpd.add(++_count);
  }
}

final _counter = _Counter(5);

Сделаем класс StatelessWidget


/// Сделаем класс "без состояния"
class MyHomeRxPage extends StatelessWidget {
  final title;

  /// ! - Можно сделать константным классом
  const MyHomeRxPage({Key key, this.title}) : super(key: key);
...

Обернем виджет отображения в StreamBuilder и изменим вызов метода инкремента


            StreamBuilder<int>(
                stream: _counter.onCounterUpd,
                builder: (context, snapshot) {
                  return Text(
                    '${snapshot.data}',
                    style: Theme.of(context).textTheme.display1,
                  );
                }),
...
      floatingActionButton: FloatingActionButton(
        onPressed: _counter.incrementCounter,
...

Вот и все. Полностью это выглядит так


import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:rxdart/rxdart.dart';

class _Counter {
  int _count;

  int get count => _count;

  _Counter(this._count)
      : this.onCounterUpd = BehaviorSubject<int>.seeded(_count);

  /// Создадим евент.
  final BehaviorSubject<int> onCounterUpd;

  /// Вынесем инкремент за пределы виджета, добавим генерацию события.
  Future incrementCounter() async {
    onCounterUpd.add(++_count);
  }
}

final _counter = _Counter(5);

///
class MyHomeRxPage extends StatelessWidget {
  final title;

  /// ! - Можно сделать константным классом
  const MyHomeRxPage({Key key, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke "debug painting" (press "p" in the console, choose the
          // "Toggle Debug Paint" action from the Flutter Inspector in Android
          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            StreamBuilder<int>(
                stream: _counter.onCounterUpd,
                builder: (context, snapshot) {
                  return Text(
                    'You have pushed the button ${snapshot.data} times:',
                  );
                }),
//            Text(
//              'You have pushed the button this many times:',
//            ),
            /// 6.
            StreamBuilder<int>(
                stream: _counter.onCounterUpd,
                builder: (context, snapshot) {
                  return Text(
                    '${snapshot.data}',
                    style: Theme.of(context).textTheme.display1,
                  );
                }),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _counter.incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

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


            StreamBuilder<int>(
                stream: onCounterUpd,
                builder: (context, snapshot) {
                  return Text(
                    'You have pushed the button ${snapshot.data} times:',
                  );
                }),
//            Text(
//              'You have pushed the button this many times:',
//            ),

и вуаля!


Для сравнения попробуйте сделать это же с InheritedWidget, или другим паттерном.


Итак, надеюсь, я показал, что


  • Реактивность это очень несложно. Гораздо проще InheritedWidgets, BlocProvider, etc.
  • Реактивность не про оффлайн/онлайн. Она про архитектуру. Как я показал, в самых простых случаях даже не нужно вносить дополнительные классы, чтобы ее применять.
  • Реактивность это отзывчивые UI, быстрое расширение функционала, изящное разделение кода на слои любого типа: MVC, MVP, MVI, MVVM, все, что пожелается.

Код примера (ветка iter_0004_rxdart)


Отредактировано часом позже
Зря сделал слишком просто, получил щелбанов за "глобальные переменные" и неправильное понимание инициализации BehaviorSubject, исправился

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


  1. andrew8712
    08.11.2019 07:26

    /// 4. Вынесем инкремент за пределы виджета...

    И получим глобальную переменную onCounterUpd и глобальную функцию incrementCounter(). Вот только зачем?


    1. rookie_cruekie Автор
      08.11.2019 07:28

      Обратите внимание, это для простоты восприятия. Никто не мешает вам сделать класс, и убрать его в business-слой.


      class _Counter {
        int count;
      
        _Counter(this.count);
      
        /// 3. Создадим евент.
        final onCounterUpd = BehaviorSubject<int>();
      
        /// 4. Вынесем инкремент за пределы виджета, добавим генерацию события.
        Future incrementCounter() async {
          onCounterUpd.add(++count);
        }
      }
      


  1. epishman
    08.11.2019 08:16

    Не знаю, для меня как-то естественней смотрится Provider, там по крайней мере понятно где хранить стейт — прямо в дереве виджетов. А эта реактивщина заставляет выносить стейт куда-то вовне, и дискутировать по этому поводу.


    1. rookie_cruekie Автор
      08.11.2019 08:29

      Дело в том, что реактивщина не только про стейты. Ведь она позволяет обмениваться событиями между любыми объектами, и невизуальными в бизнес-слое, и все это в едином стиле. Кроме того, мощь reactive-way проявляется и в потоковой обработке данных, используя операторы. Внося реактивность в проект даже на стадии обмена сообщениями, можно впоследствии постепенно начинать применять ее все шире.


      1. epishman
        08.11.2019 09:01

        Я согласен. Но в простых приложениях нет асинхронных потоков, а считать клики от кнопки потоком, это немного притянуто...


        1. rookie_cruekie Автор
          08.11.2019 14:55
          +1

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


          /// делаем задержку в 3 секунды, с уведомлением через каждую секунду.
          class _Counter {
            int _count;
            /// Счетчик обратного отсчета
            int _countdown = 0;
          
            int get count => _count;
          
            _Counter(this._count)
                : this.onCounterUpd = BehaviorSubject<int>.seeded(_count),
                  this.onCountdownUpd = BehaviorSubject<int>.seeded(0);
          
            final BehaviorSubject<int> onCounterUpd;
          
            /// Евент обратного отсчета
            final BehaviorSubject<int> onCountdownUpd;
          
            /// Вынесем инкремент за пределы виджета, добавим генерацию события.
            Future incrementCounter() async {
              if(_countdown <= 0) {
                onCounterUpd.add(++_count);
                /// Запуск таймера, с вочдогом и генерацией евентов.
                _countdown = 3;
                onCountdownUpd.add(_countdown);
                Observable
                    .periodic(Duration(seconds: 1), (_) => --_countdown)
                    .take(3)
                    .listen((e) => onCountdownUpd.add(_countdown));
              }
            }
          }

          после чего делаем FAB реактивным,


                /// Кнопка стала реактивной
                floatingActionButton: StreamBuilder<int>(
                  initialData: _counter.onCountdownUpd.value,
                  stream: _counter.onCountdownUpd,
                  builder: (context, snapshot) {
                    return FloatingActionButton(
                      onPressed: snapshot.data <= 0 ? _counter.incrementCounter : null,
                      tooltip: 'Increment',
                      backgroundColor: snapshot.data <= 0
                          ? Theme.of(context).primaryColor
                          : Colors.grey,
                      child: Icon(Icons.add),
                    );
                  }
                ), // This trailing comma makes auto-formatting nicer for build methods.

          и добавляем рекативное же упоминание


                      /// Реактивная надпись
                      StreamBuilder<int>(
                          stream: _counter.onCountdownUpd,
                          builder: (context, snapshot) {
                            return Text(
                              'Rest ${snapshot.data} seconds',
                              style: Theme.of(context).textTheme.title,
                            );
                          }),


          1. epishman
            08.11.2019 23:47

            Спасибо, убедили, реактивщина действительно правильный подход. Я просто на днях внезапно разлюбил флаттер, уж слишком код получается многословным и нечитаемым, особенно верстка, похоже вернусь назад на реакт и веб-компоненты, там хоть понятно все, а тут каждый квартал новые классы появляются, новые подходы. Вроде и работу по нему найти реально, но что-то неэстетичное есть в этом языке и фреймворке, уже сколько раз — загораюсь, начинаю что-то писать, и потом бросаю под тяжестью новых ненужных знаний :)


            1. rookie_cruekie Автор
              09.11.2019 18:01

              Удачи в работе. Надо быть ищущим, и любознательным.


  1. cutzmf
    08.11.2019 08:41
    +1

    Автор непонимает основ Rx и Flutter
    Ошибки
    1. инициализация значения в BehaviorSubject не через named конструктор .seeded(initValue)
    2. эта ошибка вытекла из первой, но показала непонимание основ Flutter.
    Нельзя ничего инициировать в методе build() любого типа виджета


    1. rookie_cruekie Автор
      08.11.2019 08:43

      Да, согласен, косяк. Даже два.


      1. Поправимо
      2. Не отменяет самого принципа и эффективности применения


      1. cutzmf
        08.11.2019 09:13

        по пункту 2
        api.flutter.dev/flutter/widgets/StatelessWidget-class.html

        The build method of a stateless widget is typically only called in three situations: the first time the widget is inserted in the tree, when the widget's parent changes its configuration, and when an InheritedWidget it depends on changes.


        api.flutter.dev/flutter/widgets/StatelessWidget/build.html
        The implementation of this method must only depend on:
        the fields of the widget, which themselves must not change over time


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

        Ты не читал документацию или не перечитал с пониманием


        1. andrew8712
          08.11.2019 09:53

          Может, обороты-то сбавите, не? Высокомерие так и хлещет.
          Автор статьи пишет, старается. Да, не все 100% идеально, а вы тут сразу тыкать начали, как вахтер какой-то


          1. cutzmf
            08.11.2019 10:01

            Вы пришли из телеги, сэр, здесь обороты нормальные,
            ошибки обозначены и описаны, показано что надо изменить.

            Не нагнетайте


  1. cutzmf
    08.11.2019 09:24
    +1

    поправь

    final onCounterUpd = BehaviorSubject.seeded(count)


    убери из конструктора
    _Counter(this.count) {
    onCounterUpd.add(count);
    }


    убери из StreamBuilder'а
    StreamBuilder(
    initialData: _counter.onCounterUpd.value,
    stream: _counter.onCounterUpd,


  1. pretorean
    08.11.2019 09:42
    +1

    Как демонстрация идеи хорошо.
    Реквестирую следующую статью, где будет проиллюстрирована эта идея на примере какой то популярной архитектуры.


    1. rookie_cruekie Автор
      08.11.2019 09:58
      +1

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


      1. cutzmf
        08.11.2019 10:06

        habr.com/ru/post/448776
        Вы в статье против BLoС, но вы не в курсе что flutter_bloc — это дериватив MVU(родоначальник подхода ELM TEA)
        guide.elm-lang.org/architecture

        In fact, projects like Redux have been inspired by The Elm Architecture, so you may have already seen derivatives of this pattern


        MVI это то же дериватив MVU и бойлерплейта там тоже много

        flutter_bloc, MVU(TEA), Redux, MVI эквивалентны (просто разные реализации)

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


        1. rookie_cruekie Автор
          08.11.2019 10:17

          Да я в курсе и нет, я не против. Каждый выбирает для себя. Я за стиль, простоту, изящество решений, по мне так этому всему отвечает именно ReactiveX. Он закрывает для меня почти все вопросы по взаимодействию и с View и между всеми слоями архитектуры с минимумом кода и максимумом выразительности. Это я еще не беру в расчет мощь его цепочек операторов.


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


          MVI я не использую в чистом виде по этой же причине, но мне импонирует immutable-way, и передача упакованного состояния в одном флаконе. Позже я черкану статью о своей реализации.


          1. cutzmf
            08.11.2019 10:24

            flutter_bloc (дальше упоминаю как BLoC) Felix'a Angelo это чуть по-другому реализованный MVI от Dorfman'а
            UDF + Rx

            Как тебе угодил MVI, но не угодил BLoC?

            Intent -> Model -> View
            Event -> State -> View

            эквивалентные вещи и через тот же Rx


            1. rookie_cruekie Автор
              08.11.2019 10:42

              Приходится повторяться специально для Вас, что мне не нравится раздутость кода, в которое обернуты оба решения, и я не применяю MVI в том виде, в котором он есть. Вы не вчитываетесь, а видимо, просто еще на утреннем автовзводе от общения в telegram.
              Я бы с удовольствием увидел Ваши решения счетчика на BLoC, MVU, и сравнить количество строк, при одинаковом результате. Попробуйте принять, как данность, что это статься просто про другой подход к решению задач программирования. Пожалуйста. Спасибо.


              1. cutzmf
                08.11.2019 10:54

                меньшее количество строк != читабельность && easy поддержка

                BLoC & MVI смысл один один поток на вход, один на выходе
                + предсказуемый стейт

                Вы пишете свой велосипед, который может потерять этот +, если не один единственный поток на вход(очередь событий)
                Почитайте про синдром NIH

                Жду статью про велосипед дериватив MVU(TEA), Redux, MVI, BLoC


              1. cutzmf
                08.11.2019 10:58

                Я не пишу решения счетчика на BLoC, оно уже есть от оригинала(Felix) в документации.

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


              1. plugfox
                08.11.2019 15:06

                Вы не правы.
                В своей статье Вы сравниваете архитектуру (стейт менеджмент), виджет (ui элемент), di и все это с оберткой над dart:async. Прям целая каша в голове.

                Никто не сомневается, что можно написать hello world вообще не оперируя такими понятиями и вообще используя только setState и Future builder.

                Собственно, в этом и состоит к вам претензия.

                PS: если хотите, распишу вам подробнее, зачем нужен DI (Provider, оберточка над Inheriting Widget), зачем стейт менеджмент (BLoC, который, между прочим, на rxdart).


                1. rookie_cruekie Автор
                  08.11.2019 15:17

                  Давайте так, спасибо конечно, но мне тут не надо ничего расписывать, и так уже простыня-простынища.
                  Предлагаю Вам написать статью, в которой Вы представите свой взгляд. свои подходы, и свое видение.
                  Я достаточно знаю и про DI, и про BLoC, статья не об этом. Статья о том, что можно эффективно и просто достигать целей, используя очень простые решения. Я писал и с использованием Provider, и с использованием MVU, и в конечном итоге остановился на том, что безумное количество дополнительного кода не может служить оправданием того, что я использую хайповые решения. Видите ли, я сам себе режиссер, и мне не надо ходить на собеседование и ожидать тупых вопросов типа "а знаете ли вы вот эти стопицот модных технологий?", а в реальной жизни дауншифтинг на rxdart уменьшил мой код вдвое(!), не затронув производительность.


                  Вот о чем статья. Для сравнения технологий, практик, паттернов. Так что пишите свою, (и наполучаете от хейтеров в карму :) ).


                  PS. Да, вдогон. Про DI, Provider я вообще не упоминал, а на КДПВ указан концепт провайдера для BLoC, по аналогии с тем, что в этой статье. Пожалуйста, читайте статьи, которые комментируете, а не додумывайте свое за автора. Спасибо.


            1. cutzmf
              08.11.2019 13:58
              -1

              habr.com/ru/post/474968/#comment_20862172

              минусатор, есть аргументированные возражения этому коменту?


  1. cutzmf
    08.11.2019 11:04

    Cделать приватным final BehaviorSubject onCounterUpd;
    Наружу только Observable или Stream, потому что я могу миновать incrementCounter() и сунуть туда какое захочу value и оно минует вашу логику

    Это как раз вопрос велосипеда, он уже несостоятелен и его приходится рихтовать.

    Зачем здесь async функция? Future incrementCounter() async


    1. rookie_cruekie Автор
      08.11.2019 16:47

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


      Но раз уж Вы за чистоту рядов, то что скажете про реализацию toMap/fromMap в дарте "из коробки"? Отсутствие как минимум признака типа в карте делает ее худо-бедно применяемой только в узком локальном контексте, не позволяя передавать никуда дальше, ибо иначе может приводить к интересным коллизиям типа


      test('Когда все пошло не по плану', () async {
      
          // в точке А.
          final manBefore = Straight(name: 'Serg', age: 25);
          final map = manBefore.toMap();
          print('man before is: ${manBefore.runtimeType} as $map');
      
          // в точке Б.
          final manAfter = Gay.fromMap(map);
          print('man after is: ${manAfter.runtimeType} as $map');
      
        });
      
      LOG:
      man before is: Straight as {name: Serg, age: 25}
      man after is: Gay as {name: Serg, age: 25}

      для схожих типов


      class Straight {
        final String name;
        final int age;
         ...
      class Gay {
        final String name;
        final int age;
         ...

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


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


  1. Rikudoxxx
    08.11.2019 12:50

    Реактивность это очень несложно. Гораздо проще InheritedWidgets, BLoC Provider, etc.

    Насколько я могу судить то базово ты как раз таки реализовал BLoC


  1. tetraset
    08.11.2019 14:57

    По мне так для управления состоянием лучше всего подходит пакет: pub.dev/packages/flutter_bloc
    Тут хорошо объясняется как им пользоваться: www.youtube.com/watch?v=hTExlt1nJZI

    Для данного пакета уже создана целая инфраструктура:
    pub.dev/packages/hydrated_bloc
    pub.dev/packages/bloc_test
    pub.dev/packages/sealed_flutter_bloc


    1. rookie_cruekie Автор
      08.11.2019 15:01

      Прекрасно, когда есть из чего выбрать. Мне фломастеры из rxdart-коробки больше по душе, потому что я так или иначе последние три года пользуюсь ReactiveX в составе RxJava, RxQt, RxCpp в той или иной степени, с их подкупающей мощью обработки данных посредством цепочек операторов, и поэтому я убиваю двух зайцев, используя rxdart как управление состоянием UI и для работы с данными.