GetX & Flutter: UDF with Rx-variables
GetX & Flutter: UDF with Rx-variables

Замечание: Почему скриншоты вместо сниппетов кода? Просто красивее выглядит. Весь упоминаемый код находится на Github gist, а сама библиотека на pub.dev.

Reactive State Management в GetX сделан очень изящно и реализуется буквально в пару строк - одна в контроллере и одна во View, как-то так

Однако есть проблема. Этот подход нарушает принцип UDF, да в общем-то и все наперечет архитектурные паттерны приложений MVC/MVVM и далее по порядку в части взаимодействия View с состоянием модели. View не должна иметь возможность напрямую изменять это состояние. А в примере выше именно так и происходит.

Модель никак не модерирует состояние, View его изменила и тут же получила обратно. В данной реализации с этим ничего не поделать, так как переменная clickCounter публична.

Конечно, в GetX как всегда находится решение, и оно достаточно простое - спрятать переменную за геттером и сеттером:

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

В общем-то, на этом можно и остановиться, решение рабочее. Но...

Переменная разрослась до трех сущностей. А если кроме этого потребуется обращаться к ее стриму, то придется добавить еще и его:

Управлять 3-4 сущностями для каждой переменной то еще занятие. Многословно, чревато ошибками, не изящно.

Все эти вопросы (и не только) призван решить GetRxDecorator.

GetRxDecorator как элегантный путь к UDF

Вместо четырех переменных как описано выше, можно написать что-то вроде этого

Мы заменяем 4 переменные на одну, инкапсулируя поведение в стиле ООП & UDF, исключая непреднамеренное нарушение доступа к изменению напрямую.

Этот подход реализует паттерн Decorator вокруг Rx-переменной. В самом простом случае достаточно написать var rxVarInt = 1.obsDeco() вместо var rxVarInt = 1.obs.

Дополнительные возможности декоратора

  1. Опциональный параметр args в сеттере и его колбеке

Он позволяет реализовывать нестандартную логику присвоения в определенных случаях.

К примеру, когда изменение значения происходит только в случае если аргумент либо не тип Int, либо значение агрумента больше 1, то сеттер выглядит как-то так

а по тестам видно, что присвоение происходит верно

  1. Параметр oldValue в колбеке сеттера

В колбек сеттера передается текущее значение переменной через параметр oldValue. Используя его вместо (вместе) newValue можно реализовывать необычные алгоритмы, основанные на текущем значении переменной, типа "Гипотезы Коллатца". Это может выглядеть как-то так:

(См. в тесте ‘Collatz conjecture setter test’ и в примере в библиотеке)

  1. Опциональный параметр forceRefresh.

Этот параметр позволяет форсировать обновление неизмененной переменной.

Исходники и библиотека

Исходники можно посмотреть на GitHub gist

Так же решение оформлено, как часть библиотеки на pub.dev

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


  1. rafuck
    14.05.2022 20:45
    +1

    Какой-то высосанный из пальца пример, как мне кажется. Зачем в представлении делать counter++ и дальше говорить о каком-то UDF? Ведь в реальности во вью будет какой-нибудь

    onPressed: controller.like
    И что-то вроде
    ObxValue(builder, likesObserver)

    В любом случае, за «сеттером» controller.like спрячется вся логика. Можно, конечно, спрятать ее за оператором инкремента, чтобы было красиво, но что-то не кажется, что игра стоит свеч.

    P.S. И, может быть, не надо вставлять код картинками?


    1. rookie_cruekie Автор
      15.05.2022 12:48

      Зачем в представлении делать counter++ и дальше говорить о каком-то UDF? 

      Потому что под капотом декоратора находится переопределение оператора ++ (полная мимикрия под стандартный Rx<int>, а как же иначе - это же паттерн "декоратор"), и инкремент значения не изменится напрямую, а только лишь после взаимодействия с логикой модели, посмотрите код в гисте, там кстати полно юнит-тестов и есть пример для понимания работы.
      Вкратце поток логики будет такой:

      1. View скомандовала, что хочет инкремента

      2. Сеттер (часть модели) провел операции над этой командой, приняв решение

      3. Значение реактивного поля изменилось

      4. View реактивно перерисовалась

      Никак иначе чем через модель, изменение и команда на перерисовку не произойдет. Ровный такой UDF.

      Ведь в реальности во вью будет какой-нибудь...

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

      Видимо речь идет о лайках. Пусть количество лайков за каждый клик зависит от "веса клиента", например, каждый клик админа будет добавлять по два лайка. Налицо логика, которую надо прятать от View. Строим вашим методом

      /// Модель для счетчика лайков.
      class LikesController extends GetxController {
        /// Чисто обыгрыш логики.
        static const _isAdmin = true;
      
        /// Сама переменная, которая не может быть приватной, следовательно
        /// доступна для нарушения инкапсуляции и логики.
        var likeCounter = 0.obs;
      
        /// Метод изменения переменной.
        void like() {
          if (_isAdmin) {
            likeCounter.value = likeCounter() + 2;
          } else {
            likeCounter.value = likeCounter() + 1;
          }
        }
      }
      
      /// Где-то на View
      ObxValue(
        (data) => ElevatedButton(
          onPressed: c.like,
          child: Text('Std Likes: $data'),
        ),
        c.likeCounter,
      ),

      Ну вроде все норм, только есть нюанс. Переменная likeCounter публична (а как иначе, если только не накладывать ограничения на размещение контроллера в пакедже View, ну или другие фокусы). Соответственно, никто не мешает постучаться в нее напрямую:

      ObxValue(
        (data) => ElevatedButton(
          // Уппс, ай дид ит эген
          onPressed: () => c.likeCounter.value = c.likeCounter.value + 1,
          child: Text('Std Likes: $data'),
        ),
        c.likeCounter,
      ),
      

      и вся логика превратится в тыкву.

      Давайте теперь применим GetRxDecorator.

      /// 4-в-1: Rx-переменная, геттер, сеттер + стрим на всякий случай.
      /// Строгая инкапсуляция _значения_ переменной.
      final likeCounterDeco = 0.obsDeco(setter: (oldValue, _, __) {
        if (_isAdmin) {
          return oldValue + 2;
        } else {
          return oldValue + 1;
        }
      });
      

      А в клиенте будет как-то так

      Obx(
        () => ElevatedButton(
          onPressed: c.likeCounterDeco,
          child: Text('Decorator Likes: ${c.likeCounterDeco}'),
        ),
      ),

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

      P.S. И, может быть, не надо вставлять код картинками?

      Да, зря я так. Надо будет поправить, согласен. Но с другой стороны это же просто сниппеты, а в gist и в pub.dev есть полный код, примеры и тесты.


  1. Mitai
    15.05.2022 11:37

    1. rookie_cruekie Автор
      15.05.2022 13:10

      Это старая история, да, не буду холиварить. Я ж говорю, статья для тех, кто использует GetX именно в плоскости их офигенного State Management. Каждый выбирает для себя. Полтора года использования этой либы на пяти-шести проектах - полет нормальный. Немного пилим напильником, но не принципиально. Если что, мы не понаслышке знаем про Provider, Redux, setState (хехе), так что наш выбор управления состоянием осознанный.