Model-Widget-WidgetModel, или какой архитектурой пользуется Flutter-команда в Surf

    Привет, меня зовут Артём. Я руководитель Flutter-разработки в Surf и со-ведущий FlutterDev подкаста.


    Flutter-отделу в Surf уже больше года. За это время мы сделали несколько проектов: от маленьких служебных, до полноценных е-коммерс и банкинга. Как минимум, многие из вас уже могли видеть приложение аптеки «Ригла». В статье я расскажу про недавно вышедший пакет mwwm — архитектуру, на которой построены все наши проекты.



    Что такое MWWM?


    MWWM — это архитектура и реализация паттерна Model-View-ViewModel, которую мы в Surf переложили на Flutter. Мы заменили слово View на Widget, потому что View не очень часто используется во Flutter и так будет нагляднее для разработчиков. Главное, что она позволяет делать — разделять вёрстку и логику, как бизнесовую, так и презентационного слоя.


    Немного истории


    Почему именно MWWM мы используем в Surf? Давайте вернёмся к началу 2019 года, когда у нас зародился Flutter отдел. Что было на тот момент?


    Flutter зарелизился буквально месяц назад: хайп начал подниматься, активных игроков на рынке и в опенсорсе пока нет. Отличное время чтобы залететь, занять неплохое место в индустрии и завоевать внимание, не правда ли? Фреймворк молод, коммьюнити развивается и это открывает все дороги.


    В это время от Android-отдела Surf отделяется Flutter направление. И сразу же стоит задача сначала заложить основу для наших будущих приложений, а потом уже кодить. Конечно же, одним из основных моментов здесь являлся выбор архитектуры.


    В начале 2019 года нет явно выделяющегося мнения в комьюнити какую архитектуру использовать (хотя и сейчас ведутся активные холивары). Да, есть основные концепции: BLoC, Redux, Vanilla, MobX и тд. Самыми массовыми являются BLoC и Redux. Замечу, что речь ведётся не о пакетах, а о концепции.
    Итак, встал вопрос BLoC или Redux нам взять? Или же придумать нечто своё?


    Почему мы не BLoC?


    Business Logic Component — отличная концепция. Блоки кода, по факту, «чёрные ящики» с некоторым входным воздействием и выходным потоком, внутри которых крутится бизнес-логика на чистом Dart — это просто потрясающе. Чистый Dart для кросс-платформенного переиспользования с вебом (да, тогда было далеко до Flutter for Web и сайты писали на Angular Dart, при необходимости). Удобство использования и достаточная изоляция самих блоков. Круто одним словом. Но есть одно «но»: где писать логику презентационного слоя? Где писать навигацию? Как работать с чисто UI событиями?


    Чуть позже вышел в релиз всем известный Bloc от Felix Angelov. А также flutter_bloc. Многие стали рассматривать блоки как логику презентационного слоя, но это уже не вязалось с аббревиатурой. Сама же библиотека не давала указаний, где писать подобную логику, например, валидировать поля ввода. Это было неприемлемо для выбора хорошей архитектуры.


    Redux?


    Как я написал выше — мы выходцы из Android разработки. В то время в мире царили Clean Architecture, подходил MVVM. Веб-технологии были чем-то чуждым и непонятным. Redux мы быстро отмели: на него надо было переучиваться довольно долго и отчасти менять мышление, к которому мы привыкли во время Android-разработки с Rx и CleanArchitecture.


    Отбросив основные концепции и принимая во внимание реалии Surf, мы поставили задачу создать архитектуру, которая позволит Android-разработчикам из Surf при необходимости быстро конвертироваться во Flutter-разработчиков. И чтобы при необходимости Flutter-разработчики могли быстро перейти на Android, потому что архитектура понятна, знакома, а язык выучить несложно. Так и появился Model – Widget – WidgetModel.


    Model-Widget-WidgetModel


    Выглядит он примерно вот так.



    Эту картинку можно видеть на странице в GitHub. Здесь есть несколько основных частей.


    • Widget-UI — та самая вёрстка.
    • WidgetModel — логика этого самого UI и его состояния.
    • Model — контракт с сервисным слоем. На данный момент это экспериментальная часть. Исторически у нас использовались Interactor’ы напрямую в WM.

    Пройдёмся по каждой из этих частей поподробнее.


    Widget в нашем контексте — полностью пассивная вёрстка. Максимум что допускается в ней – наличие условного оператора, когда мы пишем там if (условие), покажи мне это, иначе – покажи мне loader. А всё остальное: вычисления этих условий, обработка нажатий и так далее — уходят в WidgetModel. Причём виджетом может быть как целый экран, так и конкретный маленький элемент на экране со своим состоянием.


    Сразу замечу, кроме MwwmWidget (буду называть их так, чтобы не путаться) мы также используем обычные Flutter-виджеты. Потому что куда без них? Есть места, где нет необходимости усложнять простенький переключатель ради архитектуры.


    WidgetModel – это, по сути, состояние виджета и описание логики его работы. Что входит в логику? Это обработка тех или иных действий пользовательского интерфейса. Это обращение к любым другим слоям, которые стоят выше. Это вычисление тех или иных значений, обработка, mapping данных, необходимых для вёрстки. В общем-то, все то, чем должна заниматься стандартная ViewModel.


    Рассмотрим на небольшом примере.



    Допустим, у нас есть некоторый экран и у него есть некоторое состояние и кнопка, которая входит в сеть. Widget экрана будет содержать только вёрстку. При этом состояние его и реакция на взаимодействия в WidgetModel. Замечу, что WM описывает также некоторые микросостояния на экране. Этими микро-состояниями могут являться Stream’ы.


    class SomeWidgetModel{
    
        final _itemsController = StreamController<List<Items>>.broadcast();
        get items => itemsController.stream;
    }
    

    Внутри команды мы как раз используем стримы (а точнее обёртку над ними) внутри виджет-модели. Поэтому по форме она очень сильно похожа на конценпцию BLoC’а. Это коробка с input/output. Каждый input — это действие пользователя или событие, output — данные, влияющие на UI. Как я уже сказал, каждый стрим — микросостояние внутри виджет-модели. Такими состояниями могут быть маленькие элементы: кнопка, которая дизейблится при каких-то условиях; поток текста с экрана и т.д.


    Вспомним о кнопке на экране. Её состояние может быть одним из таких стримов и тогда она просто будет обернута в StreamBuilder на экране. Но надо понимать, что в этом случае логика кнопки должна быть довольно проста.


    Stream<bool> get isBtnDisabled => btnController.stream;

    А теперь представим, что такая кнопка встречается ещё в 5 местах по всему приложению. И кроме некоторого изменения состояния на, к примеру, дизейбл, она ещё и триггерит запрос в Сеть. Причём запрос один и тот же, у него лишь разные аргументы. В этом случае каждый раз копипастить логику из экрана в экран, прописывать запросы и вообще загрязнять виджет-модель не очень хорошо. Гораздо лучше просто выделить эту кнопку в Widget+WidgetModel и целиком и полностью переиспользовать из экрана в экран, передавая те или иные параметры на вход.
    Ещё одно важное замечание, что виджет-модель – единственный способ привести виджет или часть виджета в некоторое состояние. Что под этим надо понимать? В Flutter, например, мы можем передавать некоторые аргументы виджет в конструктор. Это полезно, если вы используете stateless-виджеты.


    Но что у нас получается при использовании WM? Самим состоянием виджета должна управлять именно виджет-модель. Это главный источник. Если передаём какие-то данные в виджет, то мы должны передать их в WM, и только из неё их взять. Напрямую эти данные использовать в виджете нельзя, потому что иначе получается два пути приведения виджета в состояние. И это вас в конечном счёте может запутать или привести к очень неочевидным багам, которых лучше не допускать.


    Поток данных W-WM



    Какой поток данных происходит между виджетом и виджет-моделью? Из виджета идут некоторые действия — события пользовательского интерфейса. Из виджет-модели идут некоторые состояния в виджет. Состояния могут быть в виде потоков (так делаем мы в Surf), могут быть просто переменными. На эти состояния мы подписываем части виджета с помощью StreamBuilder’ов.


    //…
    child: StreamBuilder<Item>(
      stream: wm.item,
      builder: (ctx, snapshot) => //...
    ),

    Важно заметить, что связь между Widget и WidgetModel явно не прописана внутри пакета. Мы не стали сужать возможности фреймворка и дали свободу пользователям пакета самим определять связь. При этом, тот подход, что работает в нашей компании является отдельным пакетом на pub.dev.


    Relation


    Мы рекомендуем использовать наш пакет MWWM вместе с модулем Relation. Relation – это связь, которую мы используем между виджетом виджет-моделью. Это просто семантическая обёртка над потоками. У нас есть некоторые потоковые состояния в виде StreamedState и действия под названием Action. С Relation довольно просто работать.


      final toggleAction = Action<int>();
    
      final contentState = StreamedState<int>(0);
    
    //…
    subscribe(toggleAction.stream, (data) => contentState.accept(data));

    Обработка ошибок



    В больших проектах очень важно правильно обрабатывать ошибки. В рамках MWWM предусмотрен специальный интерфейс ErrorHandler, который обязательно поставляется в WM. WidgetModel перехватывает ошибки, которые приходят из сервисного слоя (или происходят внутри презентационного), и передаёт их обработчику. Автоматическая обработка происходит при использовании методов WM с постфиксом ...HandleError().


    subscribeHandleError(someAction, (data) => doOnData());
    
    doFutureHandleError(someFuture, (data) => doOnData());

    Реализацию ErrorHandler можно посмотреть в примере проекта.


    Model


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



    Рассмотрим подробнее.


    Model — контракт и унифицированное АПИ для взаимодействие с сервисным слоем. По факту это конкретная сущность с двумя методами: perform и listen. При этом в модель передаётся некоторый список Performer’ов, но обо всём по порядку.


    Суть взаимодействия основана на том, что WM говорит: «Model, хочу внести Изменение (совершить действие) в сервисный слой. Сделай это» и ждёт результата. Такая конструкция позволяет полностью абстрагироваться друг от друга при написании кода, просто условившись на контракте.


    Change


    Это первая часть контракта и это класс, который описывает намерение что-либо изменить, получить и тип получаемого результата. Он может содержать данные, но не может содержать никакой логики. Просто data-класс в терминах того же Kotlin.


    class Authenticate extends FutureChange<Result> {
      final String name;
    
      Authenticate(this.name);
    }

    Performer


    Вторая часть контракта – Performer. Performer – это реализация логики. Самый близкий аналог перформера — UseCase. Это такая функциональная часть контракта. Если Change – это параметры, название метода, то Performer – тело метода и, по сути, код, который исполняется по этому Change. В перформер могут поставляться любые сервисы, бизнес-логика, интеракторы и так далее. Такая конструкция полностью отвязывает виджет-модель от реализации этой бизнес-логики.
    Идеальный перформер – это только одно действие. То есть это такой очень атомарный кусок кода, который просто выполняет что-то одно и отдаёт результат. Performer однозначно связан с типом Change.


    class AuthPerformer extends FuturePerformer<Result, Authenticate> {
    
      final AuthService authService;
    
      AuthPerformer(
        // сущности, которые нужны перформеру для работы
        this.authService,
      );
    
      Future<Result> perform(Authenticate change) {
        return authService.login(change.name);
      }
    }

    Был один метод, стало два класса


    Зачем это надо было разделять? Потому что если у интерактора был один метод, то здесь у нас получается два класса вместо одного метода. Но таким образом из виджет-модели исчезает полностью информация об интеракторах, о реализации сервисного слоя. Всё что нужно – знать, что вы хотите сделать. То есть знать Change. И предоставить Model с необходимым набором перформеров.


    Неприятный минус: если вы не предоставили Performer, то узнаете об этом только в рантайме.


    Бизнес-логика


    Тут полная свобода действий. MWWM не декларирует, как реализовать бизнес-логику вашего приложения. Рекомендуется использовать тот подход, что принят в вашей команде. В Surf мы используем CleanArchitecture, я в своём pet-проекте работаю с сервисами, которые поставляются в перформеры и там работают. Тут может быть действительно всё, что угодно. Вся суть MWWM в том, что он довольно гибок в использовании, и его можно адаптировать под свою команду.


    Стек Surf


    Наш стек в Surf – это вот такой набор пакетов для архитектуры.



    По факту для нас это один пакет под названием surf_mwwm. Если интересно посмотреть подробнее, то можно найти на нашем GitHub.


    На диаграмме:


    • injector — пакет для реализации DI. Основан на InheritedWidget. Маленький и простой.
    • relation — связь между слоями.
    • mwwm — герой этой статьи
    • surf_mwwm — всё вместе с небольшой добавочкой, специфичной для нашей команды.

    Вместо заключения


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


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


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


    Если вкратце, что мы имеем по MWWM:


    Плюсы:


    • Разделяет и изолирует все слои (UI, Presentation Logic, Business Logic).
    • Не имеет лишних зависимостей.
    • Гибко настраивается под нужды команды.
    • Позволяет описать контракт и работать параллельно.

    Минусы:


    • Слишком сложно, если пишешь «в одного».
    • Многословное описание контракта.

    MWWM – это часть нашего большего репозитория. У неё есть свой отдельный репозиторий — SurfGear. Это набор наших стандартов и библиотек, которые мы используем в Surf.


    Часть этих библиотек уже в релизе на pub.dev:



    И команда Surf не собирается останавливаться на этом.

    Surf
    Компания

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

      0
      Widget в нашем контексте — полностью пассивная вёрстка. Максимум что допускается в ней – наличие условного оператора, когда мы пишем там if (условие), покажи мне это, иначе – покажи мне loader. А всё остальное: вычисления этих условий, обработка нажатий и так далее — уходят в WidgetModel.

      Т.е. в виджете не может быть в if составного условия в духе (list.isEmpty && !loading) и подобное выносится в WM? А switch по енаму? Если например у нас три состояния нужно отобразить: список пустой и ни разу не загружался, список пустой потому что он пустой на беке, список с элементами.

      А вот микросостояния прямо то что хотелось увидеть когда в телеграм чатике как то спрашивал про «изменяемый стейт».

      Идеальный перформер – это только одно действие. То есть это такой очень атомарный кусок кода, который просто выполняет что-то одно и отдаёт результат.

      Как предполагается поступать когда есть к примеру сценарий в котором бизнес логика выглядит следующим образом: что то обрабатывается, вычисляется, загружается -> пользователю задается вопрос о том как поступить -> в зависимости от ответа выполняется одна или другая цепочка действий. Меня этот вопрос в общем то и в нативе волнует, для себя пока ответа не нашел, но у меня за подобный сценарий отвечал бы инстанс одного класса который хранится у вью модели и имеет пару методов для входа и пару-тройку колбеков на которые должна вью модель реагировать. Что то вроде мини презентера с логикой для вью модели для одного конкретного сценария. Правда с корутинами либо async/await такой сценарий упрощается до одного вызова «перформера/интерактора/юз кейса» который дергает асинхронные методы какого то интерфейса и ждет ответа от пользователя, но не нарушает ли такой подход чего либо? Или такие вот перформеры у вас в принципе никакой реакции от пользователя ждать не могут и взаимодействие между ними как то WM разруливает?

      И еще, в статье упоминается существование примера какого то
      Реализацию ErrorHandler можно посмотреть в примере проекта.

      Где его посмотреть можно? Как то ссылочку либо проморгал либо ее не было.
        0

        По поводу if'ов:
        да в идеале составные условия уходят в WM. Свитч — вполне себе может быть внутри виджета. Тут скорее надо адекватно подходит к сложности условия.


        Про перформеры:
        они про контракт взаимодействия с сервисами. Ждать реакции от пользователя будет WM и соответственно дергать перформеры и ждать от них результата(либо подписываться на некоторые события от них).


        Про ErrorHandler:
        как раз обновили пример в репо https://github.com/surfstudio/mwwm/blob/stable/example/lib/model/common/error/standard_error_handler.dart. Правда лежит на данный момент не в том слое)) Поправим.

          0
          они про контракт взаимодействия с сервисами. Ждать реакции от пользователя будет WM и соответственно дергать перформеры и ждать от них результата(либо подписываться на некоторые события от них).

          Т.е. предполагается что именно WM в зависимости от действия пользователя выберет какой именно перформер (оставшуюся часть сценария) запускать? И получается три перформера, для первой части сценария, и для двух веток, в зависимости от выбора пользователя?
            0

            WM обрабатывает события UI, и по факту это и будет действие пользователя.
            Далее она запустит перформер, который может например принимать в себя тип необходимо сценария работы, и передать ему выбор пользователя. И тот сам уже на уровне бизнеса разрулит, что ему запустить далее.


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

        0

        Что мне не нравится в MVVM подходе, V и VM получаются всегда очень сильно связаны – изменяя один, часто мы затрагиваем другой (если речь идет о чем-нибудь чуть сложнее, чем "изменить цвет кнопки"). Чаще всего предполагается, что V максимально тупой (как вы и пишете, "максимум что допускается в ней – наличие условного оператора...") и за всё отвечает VM.


        В андроиде такой подход имеет больше смысла, потому что, например, тестировать UI в нем – то еще извращение, и возможность написать юнит-тест для сложного экрана может перевесить недостаток от разрастания кода и жесткой связки V-VM.


        Во флаттере с этим попроще, поэтому мне больше нравится именно BLoC подход, когда у нас есть State, а View – это функция от этого состояния. Но эта функция совершенно не обязано быть тупой, все вещи, которые относятся ко view, должны там и находиться.


        Я бы скорее назвал этот компонент Application Logic Component (Business – это все-таки повыше уровнем), но BLoC тоже терпимо.


        Возможно, есть смысл написать по этому поводу статью, но если кратко: BLoC – это state machine с состоянием приложения/экрана/части экрана. Widget – это переложение этого состояния на UI. Если мы заменяем GUI на CLI, то BLoC в идеале вообще не меняется. А разделять дальше Widget на собственно Widget и WidgetModel мне кажется уже избыточным (во всяком случае, во флаттере). Сейчас применяем этот подход в приложении, пока полет нормальный.

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

            Вы его, часом, с redux не путаете? Это там принят один стейт на всё приложение, и мне это тоже не нравится. В BLoC (в частности, в библиотеке bloc) как раз рекомендуют делать маленькие стейты и блоки.


            пусть даже большая его часть не будет заново рендериться

            Так это как раз очень важно. Рендеринга не будет, так что и производительность не проседает. Зато получаем удобства ФП, если view – это чистая функция состояния.


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

            Вот я как раз за то, чтобы блок был не стейтом представления, а предоставлял данные, на основе которых можно построить UI.


            Например, есть экран с кнопкой "Выход". Нажимаем на кнопку, блоку отсылается событие LogOutRequested. Блок дернул менеджер авторизации, отправил какие-нибудь логи и выдал новое состояние – Unauthorized. Потом решили, что нужен диалог с подтверждением – ну так это забота UI слоя, к блоку отношения не имеет. Поэтому view при нажатии на кнопку "Выход" показывает диалог, а на нажатие кнопки "ОК" в диалоге уже отсылает тот же самый LogOutRequested. Блок в итоге не меняется совсем.

              0
              Вы его, часом, с redux не путаете? Это там принят один стейт на всё приложение, и мне это тоже не нравится. В BLoC (в частности, в библиотеке bloc) как раз рекомендуют делать маленькие стейты и блоки

              Не путаю. В тех вариантах что я видел обычно был один блок с одним стейтом на один экран (или его часть крупную).
              Так это как раз очень важно. Рендеринга не будет, так что и производительность не проседает. Зато получаем удобства ФП, если view – это чистая функция состояния.

              Рендеринга не будет, но дерево виджетов пересоздастся и флаттеру нужно будет его инвалидировать. И на GC нагрузка, и просто по памяти, особенно если стейт меняется часто.
              Например, есть экран с кнопкой «Выход». Нажимаем на кнопку, блоку отсылается событие LogOutRequested. Блок дернул менеджер авторизации, отправил какие-нибудь логи и выдал новое состояние – Unauthorized. Потом решили, что нужен диалог с подтверждением – ну так это забота UI слоя, к блоку отношения не имеет. Поэтому view при нажатии на кнопку «Выход» показывает диалог, а на нажатие кнопки «ОК» в диалоге уже отсылает тот же самый LogOutRequested. Блок в итоге не меняется совсем.

              Как по мне слишком много логики UI в таком варианте в виджет выносится.
                0
                В тех вариантах что я видел обычно был один блок с одним стейтом на один экран (или его часть крупную).

                Ну так и VM обычно на весь экран делают. Но это необязательно.


                Рендеринга не будет, но дерево виджетов пересоздастся и флаттеру нужно будет его инвалидировать. И на GC нагрузка, и просто по памяти, особенно если стейт меняется часто.

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


                Как по мне слишком много логики UI в таком варианте в виджет выносится.

                Да, возможно, вкусовщина. По мне – это логика исключительно UI слоя, поэтому ей там самое место.

                  0
                  Ну так и VM обычно на весь экран делают. Но это необязательно.

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

                  Насколько помню элемент связанный со стейтлес виджетом один черт будет помечен как грязный при обновлении ссылки на него (хотя если виджет const — то по идее такого не произойдет), а соответственно будет вызван метод rebuild который и все дерево подвиджетов перестроит.
                  Вот, даже ссылочку нашел
                  They may then be updated (via Element.update) multiple times as they become dirty (e.g., due to widget changes or notifications)

                  Вообще сорцы бы покопать флаттера, может я и ошибаюсь (хотя вызов update элемента точно rebuild содержит) но перед сном у меня этим желания заниматься нету, зато помню что в чате по флаттеру как то пришли к мысли что нет смысла с т.з. именно производительности выносить части одного виджета в отдельные стейтлес виджеты вместо вынесения в методы, так как один фиг дерево виджетов обновиться. Пусть даже элементы и рендер объекты старыми останутся.
                    0
                    VM хоть и делают на весь экран, но благодаря тому что стейт в ней разделен — можно и экран частично обновлять, и в целом манипулировать более мелкими сущностями.

                    И в блоке то же самое (не конкретно в библиотеке bloc, а в паттерне). Блок вам дает Stream, никто не мешает сделать что-нибудь типа:

                    return Widget(
                      children: [
                        StreamBuilder(stream: bloc.state.map((s) => s.substate1).distinct(), builder: (c, s) => W1()),
                        StreamBuilder(stream: bloc.state.map((s) => s.substate2).distinct(), builder: (c, s) => W2()),
                      ],
                    );

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

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

                      0
                      И в блоке то же самое (не конкретно в библиотеке bloc, а в паттерне). Блок вам дает Stream, никто не мешает сделать что-нибудь типа

                      А в таком подходе это уже ближе к MVVM/MWWM. Пусть стейт все еще один на блок. Хотя я даже как то признаки MVI с натива тут вижу. Главное различие тут в делении логики будет. Для более крупных проектов лучше по максимуму вью логику от виджетов выносить, для меньших вполне удобно как у вас делать. Имхо, конечно. При желании вполне можно варьировать. Да и тот же стейт в блоке никто не заставляет в общем то (ну, кроме библиотек) цельным делать. Можно опять же мини стейты делать по несколько на блок и стримами гонять. Тут уже граница совсем размывается.
                        +1
                        А в таком подходе это уже ближе к MVVM/MWWM. Пусть стейт все еще один на блок.

                        Собственно, у нас сначала и было что-то среднее между MVVM и BLoC. Потом стали мигрировать на более "чистый" блок. И да, согласен, различие больше в том, куда что выносить. Если MVVM подразумевает тупой V и умную VM, которая знает, как и что будет отображаться, то BLoC это особо не ограничивает, и для нас удобнее стало деление на более умный V и гораздо менее зависимое от UI состояние.

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

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