Pull to refresh

Comments 74

Эм, каким образом добовленте деалога для логаута изменяет viewModel?

Собственно flutter_bloc это и есть по сути viewModel особенно если использовать cubit

Эм, каким образом добовленте деалога для логаута изменяет viewModel?

Что делать при нажатии на кнопку – заканчивать сессию сразу или показать диалог – это логика UI, не так ли? Если мы договариваемся, что вся логика UI содержится во ViewModel, то, меняя эту логику, нам надо изменить ViewModel.

Собственно flutter_bloc это и есть по сути viewModel особенно если использовать cubit

Нет, BLoC – это паттерн для имплементации бизнес-логики (на что, собственно, намекает название – Business Logic Component). Не надо из него делать ViewModel.

Что делать при нажатии на кнопку – заканчивать сессию сразу или показать диалог – это логика UI, не так ли?

не так.

Если мы договариваемся, что вся логика UI содержится во ViewModel, то, меняя эту логику, нам надо изменить ViewModel.

не придётся. (далее код условен): MVVM пришёл (ну или точнее "популарялизивался") с C# (WPF) в общем случае в vm передаётся некий IDialogManager (dlgMan), у которого vm в callback (ICommand если речь про WPF) делается: 1) ставится проперти "IsLogoutButtonEnabled=false" 2) делается if(dlgMan.ShowConfirmation()) { sessionMan.DoBlabla(); navigationMan.DoBlabla() ... }

не так.

А что это тогда?

не придётся.

Вы же сами дальше пишете: "делается if(dlgMan.ShowConfirmation())" – и почему же это не изменение во ViewModel?

А что такого страшного, что приходится менять View и ViewModel одновременно? Суть не в том, чтобы править один файл, суть в том, чтобы поддерживать порядок в проекте в течение долгого времени. MVVM не экономит время, он не экономит и количество строк суммарно, зато каждый файл сильно меньше суммарного.

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

Почему tight coupling – это плохо, есть куча материала.

да нет изменений в view когда вы хотите вместо "по нажатию кнопки сделать навигацию" сделать "по нажатию отобразить диалог и если юзер согласен сделать навигацию и погасить сессию". Нету этих изменений. Притом неважно сколько там этих "кнопок" на вью было (а их может быть чуток побольше чем одна)...

До изменений у вас кнопка Log out, нажатие на которую вызывает vm.onLogOutClicked()

После изменений – View получает информацию isDialogVisible и рендерит диалог, во ViewModel отсылаются onDialogCanceled() и onDialogConfirmed() – если же у вас, конечно, классический MVVM, в котором View рендерит интерфейс на основании данных из ViewModel.

Тут два варианта разрулить ситуацию:

  1. view не сразу вызывает vm.onLogOutClicked(), а сначала вызывает диалог, и только при подтверждении диалога передает сигнал во ViewModel

  2. ViewModel, в методе onLogOutClicked запрашивает отрисовку диалога и ждет подтверждения, и только потом продолжает бизнес логику.

В обоих вариантах будет менятся только один класс.

ну все методы типа "dlgMan.ShowConfirmation()" не блокирующие (не зря завезли async-await).

а что касается "View получает информацию isDialogVisible и рендерит диалог", то обыкновенно диалоги (а точнее место под их размещение в разметке если угодно), блокирующие весь "ввод" (модалка), заранее существуют в корневом view по-типу "портала, где этот диалог отрисовывается", собственно с этим и взаимодействует dlgMan - он-то знает как сделать так, чтобы диалог "отобразился".

Диалоги они в целом не самый хороший пример и всегда с "хаками" делались (например на angular - там нужно указать portal где будет жить динамическая разметка диалогов). А если без хаков, и сам диалог надо рисовать в том же вью - то, конечно, там вью менять надо - ибо откуда б взяться "разметке".

Первый вариант – да, это то, что я предлагаю. Только в рамках модели MVVM получается, что UI-логика утекает из ViewModel во View.

Второй вариант – тогда ViewModel начинает подозрительно напоминать Presenter из MVP.

Первый вариант – да, это то, что я предлагаю. Только в рамках модели MVVM получается, что UI-логика утекает из ViewModel во View.

потому что так делать не надо и вы заблуждаетесь.

Второй вариант – тогда ViewModel начинает подозрительно напоминать Presenter из MVP.

ничуть. в MVP у вас будет код вида "loginBtn.Enabled = false; if(dlgMan.Ask()) {....}" - т.е. "P" будет взывать методы непосредственно экземпляров "V" (в данном случае - по ссылке loginBtn), тогда как в MVVM в VM будет код: "this.loginBtnEnabled = false; if(dlgMan.Ask()) {....}" - т.е. VM лишь ставит своё собственное свойство (loginBtnEnabled), а уже "V" должно быть подписано на изменение этого свойства (свойство при этом в общем случае должно уметь "сообщать" подписчикам "VM" о том что изменения были, например код на C# обычно выглядит примерно так (setter) : "set { if(this._loginBtnEnabled != value) { this._loginBtnEnabled = value; this.RaisePropertyChanged("loginBtnEnabled") }".

потому что так делать не надо и вы заблуждаетесь.

Это ваше мнение.

ничуть. в MVP у вас будет код вида...

Я не про loginBtnEnabled – с этим и так всё понятно. Я как раз про dlgMan.Ask() – диалог – это точно такая же часть UI, как кнопка, label и т.д. Почему это VM говорит диалогу появиться вместо того, чтобы поставить isDialogVisible = true?

Если честно, то у меня сложилось мнение, что у вас какое-то особое представление о MVVM. Возможно, что в нативном Android, он действительно имплементрован как-то по-особенному (а вы ссылаетесь именно на Android). Я вам советую почитать о нем на сайте Microsoft.

Почему это VM говорит диалогу появиться вместо того, чтобы поставить isDialogVisible = true

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

это точно такая же часть UI, как кнопка, label и т.д.

Не совсем точно такая же. На мобильных устройствах диалоги, чаще всего (хотя теоретически не обязательно), перекрывают весь экран и блокируют страницу. При этом, вы (как правило) видите только один диалог. Бывают ситуации, когда диалоговое окно надо показать на уровне вашего приложения (может прилетела ошибка из другой части программы). Но показывать два диалога одновременно - будет странно. Значит вам все равно надо что-то, что управляет ими всеми. И тогда вы тащите новые зависимости в ваше вью. Можно это делать? Технически - конечно. Но тогда ваше вью все разростается и разростается.

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

Между прочим, у вас вообще нигде не сказано за binding. А между прочим - это одна из ключевых характеристик паттерна. Еще раз советую побольше о нем почитать.

Если вы искренне уверены, что предложенная вами архитектура более рациональна, возможно лучше представлять ее публике в чистом виде, а не под соусом, что MVVM - антипаттерн. Я бы также подумал над лучшими примерами. Предложенные в статье лично меня не переубеждают.

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

Тот факт, что диалоги управляются неким менеджером диалогов, это техническая особенность этой платформы. Во Flutter'е диалог – это точно такая же часть UI, как я уже сказал. У вас может быть модальный диалог, несколько диалогов на странице, вообще какой-нибудь inline-диалог. Никакой специальной зависимости в Widget передавать для этого не нужно. Если так будет проще, можем заменить "показать диалог при нажатии кнопки" на "при нажатии на кнопку скрыть ее и вместо нее показать 2 кнопки – Cancel и Confirm". Тут уже, я надеюсь, точно не будет разногласий в том, что это часть структуры UI.

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

Конечно, не нравится. Я не вижу причин брать UI логику и выносить ее в какой-то класс, который ничего не должен знать про UI. Опять же, я говорю конкретно про Flutter, зачем это нужно было в других фреймворках, это другой вопрос.

Между прочим, у вас вообще нигде не сказано за binding. А между прочим - это одна из ключевых характеристик паттерна. Еще раз советую побольше о нем почитать.

А зачем про него говорить? Это же не какое-то преимущество паттерна, это техническая деталь. "The binder frees the developer from being obliged to write boiler-plate logic to synchronize the view model and view" – т.е. мы разделили код на View и ViewModel, теперь нам это надо как-то синхронизировать, поэтому вводим binding.

Еще раз скажу, я не выступая против декомпозиции кода, выделения ответственности и т.д. Я заявляю, что во Flutter'е вводить ViewModel как абстракцию над View, которая представляет из себя класс, никак не привязанный к Widget, которая занимается всей UI-логикой, а виджет становится тупым рендером данным – всё это не имеет смысла.

Но если очень сильно хочется, то можно реализовать что-то очень похожее на ViewModel в 0 строчек дополнительного кода:

class TestWidget extends StatefulWidget {
  const TestWidget({Key? key}) : super(key: key);

  @override
  _TestWidgetState createState() => _TestWidgetState();
}

class _TestWidgetState extends State<TestWidget> {
  int value = 0;          //
                          //
  void onPressed() {      //
    setState(() {         //   ViewModel
      value++;            //
    });                   //
  }                       //

  @override
  Widget build(BuildContext context) => MaterialApp(          //
        home: Scaffold(                                       //
          body: Center(child: Text(value.toString())),        //
          floatingActionButton: FloatingActionButton(         //
            onPressed: onPressed,                             // View
            child: const Icon(Icons.add),                     //
          ),                                                  //
        ),                                                    //
      );                                                      //
}

Тестировать при этом тоже можно "в лоб":

testWidgets('description', (tester) async {
    await tester.pumpWidget(const TestWidget());
    final vm = tester.state<_TestWidgetState>(find.byType(TestWidget));

    expect(vm.value, 0);
    vm.onPressed();
    expect(vm.value, 1);
  });

Дальше можно извращаться как угодно: выносить в отдельный виджет, передавать данные вниз по дереву, сделать виджет, который занимается только UI-логикой, и виджет, который занимается только рендером.

Это как на той известной картинке с паттернами ООП в ФП:

Всё, что нужно от ViewModel, во Flutter'е прекрасно реализуется встроенными виджетами, гораздо проще и безо всякого бойлерплейта.

Если вы искренне уверены, что предложенная вами архитектура более рациональна, возможно лучше представлять ее публике в чистом виде, а не под соусом, что MVVM - антипаттерн.

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

После изменений – View получает информацию isDialogVisible и рендерит диалог, во ViewModel отсылаются onDialogCanceled() и onDialogConfirmed() – если же у вас, конечно, классический MVVM, в котором View рендерит интерфейс на основании данных из ViewModel.

Простите, если глупую вещь спрошу, я с бэкэнда, а здесь в целях общего развития. Но если логика у нас в вм, то какого для кнопка должна делать чтото помимо «передать в вм событие»? В моём наивном представлении именно вм будет знать, как на это среагировать — толи сделать видимым какую-то свою часть (изменение, которое оттранслируется во вью), то ли сразу дёргать бизнес-логику. То есть вью отдаёт событие «нажата кнопка» и мы при участии уже вм либо попадаем в другое вью со своей вм, либо вм изменяет себя и вью получает событие «вот это стало видимым» если диалог почему-то, что мне странно, но допустим, интегрирован в том же вью.

То есть если мы делимся на вью и вм, то во вью логики нет. Если во вью логика есть, то мы делимся неправильно и/или над нами тяготеет наследие Дельфи

То есть если мы делимся на вью и вм, то во вью логики нет.

Именно, вот я и предлагаю не делиться. И дело не в наследии Дельфи, а в том, что во Flutter'е никаких преимуществ это не дает. На мой взгляд. Об этом и статья.

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

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

Для меня (и вроде как для Flutter'а) диалоги – это точно такая же полноправная часть UI, как кнопки, надписи и прочее. Если мы выделяем менеджер диалогов, который занимается вызовом диалогов, то мы превращаем VM в Presenter. Возможно, в WPF так принято, но во Flutter я бы это не тащил.

Диалог - это часть UI, полноправная. Но мы же тут не говорим об изменении любого UI в вашем приложении. Мы говорим об изменении конкретной страницы. А в этом случае она останется такой же.

вью про это не в курсе

А зачем вашей вью (именно экрану, где располагается кнопка выхода), знать о наличии этого диалога (это и есть, кстати, пример tight coupling)? В данном конкретном случае, нажатие на кнопку Выйти просто запускает процесс выхода. А как вы структурируете этот процесс - это другое дело. Представьте, что этот процесс состоит из нескольких стадий, или с разными опциями, возможно он требует дополнительной бизнес-логики, и все эти стадии и опции никак не влияют на тот экран с кнопкой выйти.

А зачем вашей вью (именно экрану, где располагается кнопка выхода), знать о наличии этого диалога

Затем, что это UI-логика этого экрана. И я считаю, что он должна быть во View (а точнее, в виджете).

BLoC – это паттерн для имплементации бизнес-логики (на что, собственно, намекает название – Business Logic Component). 

Вот что нам говорит оффсайт.

https://bloclibrary.dev/#/architecture?id=business-logic-layer

Think of the business logic layer as the bridge between the user interface (presentation layer) and the data layer. 

Весьма близко к функциям вьюмодели.

Соглашусь, нынче товарищи любят все переинженирить, изучить 1 паттерн и использовать везде, где не нужно, лишь бы что-то "модное" для галочки затащить и щеголять красивыми аббревиатурами при описании проекта, не понимая для какой конкретной цели использует и какие плюсы и минусы это приносит

Автор показывает, скорее, теоретическое владение вопросом и делает на его основе смелые выводы. Почему-то среди преимуществ MVVM не значится уменьшение количества строк в файлах View и ViewModel за счет их разделения, удобство командной работы над проектом за счет формализации правил архитектуры. Не отмечено также, что преимущество "переиспользование ViewModel" редко используется на практике. Фактически ViewModel имеет одно главное преимущество ради которого его используют - разделение бизнес-логики и интерфейса. Это очень важно при работе со сложными проектами. Их код удается поддерживать в состоянии, когда его можно редактировать, не добавляя сложно устраняемые баги.

BTW UI во флаттере можно использовать для всех платформ только оооочень редко. Интерфейсы iOS и Android сильно отличаются, привычки пользователей тоже. Чаще всего, единственное преимущество флаттера в том, что он позволяет одному программисту писать приложение для разных платформ. Время он может немного сэкономить, но тоже не всегда. Флаттер хорошо подходит чтобы запилить прототип, когда в команде есть человек, знакомый с ним.

BLoC на флаттере хорошо подходит для реализации похожей на MVVM архитектуры. На мой взгляд, не стоит делать "все-в-кучу-виджеты" только потому что так тоже можно.

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

Почему-то среди преимуществ MVVM не значится уменьшение количества строк в файлах View и ViewModel за счет их разделения

Само по себе уменьшение количества строк в файле – так себе преимущество, если это достигается введением дополнительной сущности и усложнением связей.

удобство командной работы над проектом за счет формализации правил архитектуры

Автор всецело "за" формализацию правил архитектуры, это не преимущество конкретно MVVM, это преимущество любой формализации.

Не отмечено также, что преимущество "переиспользование ViewModel" редко используется на практике.

Отмечено, прочитайте внимательно пункт "Переиспользование ViewModel".

Фактически ViewModel имеет одно главное преимущество ради которого его используют - разделение бизнес-логики и интерфейса.

Нет, к разделению бизнес-логики и интерфейса ViewModel не имеет никакого отношения. Бизнес-логикой в MVVM вообще занимается слой M. ViewModel – это отделение UI-логики от UI.

 На мой взгляд, не стоит делать "все-в-кучу-виджеты" только потому что так тоже можно.

Я нигде не упоминал, что нужно делать "все-в-кучу-виджеты". Совсем даже наоборот. Прочитайте внимательно пункт "Виджет-компонент".

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

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

Нет, к разделению бизнес-логики и интерфейса ViewModel не имеет никакого отношения. Бизнес-логикой в MVVM вообще занимается слой M. ViewModel – это отделение UI-логики от UI.

Вообще-то имеет. Именно VM обеспечивает непрямое взяимодействие V и M, обезпечивая loose coupling.

https://en.wikipedia.org/wiki/Model–view–viewmodel

The viewmodel of MVVM is a value converter,[1] meaning the viewmodel is responsible for exposing (converting) the data objects from the model in such a way that objects are easily managed and presented.

https://docs.microsoft.com/en-us/xamarin/xamarin-forms/enterprise-application-patterns/mvvm#viewmodel

The view model is also responsible for coordinating the view's interactions with any model classes that are required.... Each view model provides data from a model in a form that the view can easily consume.

Если мы берем паттерн MVVM, то M – это слой бизнес-логики, а V-VM – это презентационный слой (посмотрите на схему в начале статьи). Да, VM в этом случае будет дергать методы из бизнес-логики и преобразовывать ее данные.

Если мы не разделяем V и VM, у нас все еще остается слой бизнес-логики, который занимается бизнес-логикой. В условном паттерне BusinessLogic-Widget, точно так же разделена бизнес-логика и интерфейс.

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

Отделение бизнес-логики от вью - это не разнесение всего этого по разным классам. Отделение, в данном случае - это когда V и М не связаны напрямую. Именно такой вот непрямой связкой и занимается вьюмодель. Если вы объедините вью и вбюмодель в один класс, то у вас вью будет отвечать за коммуникацию с моделью.

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

Мне кажется. что вы основываете свои выводы на утверждении, что VM - это просто UI-логика. А это не так. Наличие UI-логики во вьюмодели, не означает, что там нет логики по коммуникации с моделью (что приводит нас к выводу, что это не исключительно UI-логика).

Вот именно про коммуникацию с бизнес-логикой в виджете я и говорю. Я же прямым текстом об этом пишу:

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

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

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

Вот мы и пришли к тому, о чем я говорю всю статью. Только не View, а Widget.

Если у вас сложное форматирование, никто не мешает вынести это в отдельную функцию, и написать на нее юнит-тест.

Если у вас сложный UI, никто не мешает вынести его в чистый виджет, которому все данные передаст другой виджет, который свяжется с бизнес-логикой.

Но это все ситуационное выделение классов и методов там, где это нужно. Заводить ViewModel и говорить, что вся коммуникация с бизнес-логикой осуществляется только через нее, вся UI-логика крутится только в ней, а Widget – это тупой рендер приходящих данных – это совершенно ненужное переусложнение в случае Flutter'а (отчасти потому, что с усложнением условий и требований абстрактная ViewModel начинает все больше и больше напоминать StatefulWidget).

BTW UI во флаттере можно использовать для всех платформ только оооочень
редко. Интерфейсы iOS и Android сильно отличаются, привычки
пользователей тоже.

Не соглашусь. Из коробки Flutter имеет как Material, так и Cupertino виджеты. Так что при желании можно делать UI для Android и iOS, которые будут выглядеть и работать нативно, не имея разных спецов под каждую платформу.

UFO just landed and posted this here

Кирилл, спасибо за доклад и отдельно за его расшифровку!

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

Спасибо!

Спасибо!

Сами приложения относительно небольшие, но это не разовые проекты – одно из них уже 2 года в продакшене, и оно постоянно поддерживается и обновляется. Другое мы закончили переписывать с нативного Android на Flutter около полугода назад, оно тоже все еще развивается.

В разное время над этими приложениями работало от 1 до 4 разработчиков, так что, опять же, это не one-man-show, и код должен быть понятным и поддерживаемым.

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

Это интересно!

Укладываются ли на ваш взгляд пакеты вроде graphql_flutter в такую парадигму?
Возможно этот пакет именно её и реализует? Однако, границы его применимости не вполне очевидны, не смотря на кейс авторов пакета (у них есть коммерческое приложение на его основе). Мне не удалось найти их дао, позволяющее не споткнуться на этом пути.
Знакомы ли вам примеры открытых репозиториев, придерживающихся похожего на ваш подход? Это могло бы быть полезным в дискуссии с коллегами, придерживающимися одного подхода для любых проектов.

Благодарю!

С graphql_flutter не работал, поэтому ничего не могу сказать, к сожалению. На первый взгляд – скорее нет, чем да; все-таки graphql – это слой данных, а слою данных, тем более отдельной библиотеке для его реализации, нечего делать в виджетах. Я бы, скорее всего, взял чистый пакет graphql и сам бы интегрировал, куда мне надо. Но это, опять же, просто судя по первому впечатлению, может, я не совсем понял их концепцию.

Тот же flutter_bloc, например, это, по большей части, bloc + provider, так что тут ничего не нарушается – BLoC'и отдельно и независимо от виджетов и Flutter'а в целом, и BlocProvider / BlocBuilder для встраивания их в дерево виджетов.

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

Авторы этих пакетов тоже основывают свои взгляды на своей же коммерческой практике и имеют смелость предлагать своё решение сообществу. И судя по pub.dev и github, сообщество активно откликнулось.
Понимаю, что их подход выглядит весьма радикально. Но более ли радикально, чем Flutter way по отношению к более традиционным подходам?
Всё же хочется расширять свой инструментарий, не только зрелыми, но и перспективными подходами.

Сейчас я использую чистый пакет graphql, а на использование graphql_flutter не решился и задаюсь вопросом, а не стал ли я динозавром.
Хотя опыт коллег-нативщиков с Apollo показывает, что всё состояние вполне может жить в кеше клиента graphql и тогда сущностей становится ещё меньше, функционал локализуется ещё лучше и для работы над приложением в команде достаточно понимать GraphQL API и свою часть не маленького приложения, чтобы эффективно включиться в разработку.
Наверное, это похоже на микрофронтенд.

Благодарю!

Я не считаю ни мой подход, ни подход авторов graphql_flutter чем-то радикальным или незрелым. Принципу KISS лет 50, бритве Оккама и того больше.

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

Если мобильное приложение представляет из себя рендер данных, полученных от graphql (пусть даже и сложный с точки зрения UI), то такой подход (как у авторов graphql_flutter) вполне себе оправдан. Зачем вводить дополнительный слой, если там не будет никакой полезной работы?

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

Поэтому, мой подход (про который я говорю в статье) в более практическом смысле отличается от подхода graphql_flutter. Но если говорить в более широком смысле – да, и там, и тут затрагивается принцип KISS.

А вот в случае с ViewModel и Flutter, на мой взгляд, идет усложнение без особых преимуществ, и принципу KISS это противоречит. Об этом и статья.

Ни в коем случае не хотел назвать такой подход незрелым. Не самым удачным способом назвал своё ощущение, возникающее, вероятно, от дискуссии с воинствующими адептами от Clean Architecture. Бывает не просто отрешиться от эмоций и влияния авторитета.

Ваш аргумент понятен и вопросов не вызывает. Но по-прежнему есть желание посмотреть на хотя бы средних размеров проект, желательно, не на свой :)

Чтобы так громко рассуждать про KISS НАЧНИТЕ ПИСАТЬ ПРОСТО. Выкиньте свой flutter и напишите приложение просто и влоб.

Прям вот без каких либо фреймвороков, обёрток и тому подобного. Если Ios то Swift в помощь, если андроид то ява, докажите свою приверженнность подходу, пишите простенько и нативненько.

А почему это без Flutter'а писать будет проще? Мне вот проще написать один раз, а не два. И поддерживать приложение на фреймворке с изначально декларативном подходом к UI мне тоже проще.

KISS ведь не про отсутствие любых абстракций. Он про отсутствие ненужных абстракций.

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

  1. Теперь невозможно без боли реализовать отличающиеся интерфейсы для разных платформ с учётом их специфики, просто написав несколько различных представлений одной и той же части интерфейса

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

Не раз замечал, как разработчики начинают путать логику UI и логику взаимодействия с моделью. Как по мне, логику UI (фокус, цветовые акценты, эффекты и т.д.) нужно оставлять в представлении, все остальное должно быть внедрено в представление изве. Тогда изменение модели и представления могут происходить независимо (до определенного момента, конечно).

Теперь невозможно без боли реализовать отличающиеся интерфейсы для разных платформ с учётом их специфики, просто написав несколько различных представлений одной и той же части интерфейса

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

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

Любое изменение в интерфейсе модели потребует изменений в презентационном слое. Какая разница, будет это в классе Widget или в классе ViewModel?

Не раз замечал, как разработчики начинают путать логику UI и логику взаимодействия с моделью. Как по мне, логику UI (фокус, цветовые акценты, эффекты и т.д.) нужно оставлять в представлении, все остальное должно быть внедрено в представление изве. Тогда изменение модели и представления могут происходить независимо (до определенного момента, конечно).

Что именно вы подразумеваете под логикой взаимодействия с моделью? Знание о том, какой метод из слоя бизнес-логики дернуть (aka какой запустить Use Case Interactor)?

Ну а с тем, что логику UI надо оставлять в представлении (если под этим подразумевать Widget), я как раз и не спорю.

Я вообще не из Flutter'а но мои пять копеек:
— Хорошо когда для всего есть модель, подпадающая под правила работы с моделью, но есть информация, которая для правильного отображения нужна, но бизнес-логику совершенно бессмысленно замусоривает, типа того, какой из подпунктов выпадающего списка выделен в данный момент. VM Место как раз для такой информации, а до контроллеров и модели бизнеслогики дойдёт только информация о том, что выделен другой элемент, или даже ещё более бизнесовая.
И тут дело даже не в том, что хранить это в VM плохо, а в том, что больше её девать то некуда, только внутрь View как делают все когда пишут в первый раз, а вот это уже точно плохо-паттерн.

Ну вот я пишу сильно не в первый раз, и писал раньше сильно по-разному. На этот путь я встал совершенно осознанно.

И тут дело даже не в том, что хранить это в VM плохо, а в том, что больше её девать то некуда.

Девать ее можно и, по моему мнению, даже нужно туда, где этой UI логике самое место – в UI слой. Я не говорю про то, что этот должен быть один экземпляр класса, разбивать на мелкие компоненты нужно. Но выделять отдельный слой ViewModel, который ничего не знает про View, но при этом занимается логикой этого View – это как-то странно. Опять же, зачем это нужно было в других фреймворках – понятно, но во Flutter'е таких проблем нет.

habr.com/ru/post/434582 Вот тут я поясняю за свой подход. Смысл в том, что если у вас данные уровня VM хранятся в модели (бизнес-модели или отдельной) и смазаны всей необходимой реактивностью, и у вас есть некоторые дополнительные возможности по работе с моделью, то вы можете то, что при вашем подходе невозможно.

Допустим у клиента эксепшеном упал UI клиентa с вероятностью 10-е5 (у меня реально такое было в карьере, ошибка округления флоата), ну или просто завязанное на очень неожиданные действия пользователя. QA отдел хрен вам это воспроизведёт даже если сотрёт пальцы, а вы, поскольку у вас крутая модель можете откатиться к началу транзакции сериализовать полностью модель, как M так и VM, послать её на адрес, десериализовать её у себя и повторить команду. И получите ровно тот же ексепшен, что на бою. ИМХО, очень крутая фича если у вас DAU десятки и сотни тысяч.
Другая фича — ваша модель может в любой момент сделать exportChanges с начала транзакции до текущего момента. Это прям божественно при отладки, а если M и VM сделать на общих классах, то точно то же самое у вас сможет сделать VM. Опять приятно при отладке.

Хотя, конечно, эти преимущества происходят не из MVVM а из того, каким конкретно инструментарием я буковку M у себя обложил.

Ну уж тогда и мои 5 копеек.

Смысл и суть самого паттерна MVVM оторвать данные от view, а саму view от деталей бизнес логики. Не затронут аспект когда одна view использует несколько разных ViewModel, а это очень частая практика.

Явный плюс mvvm даже не в уменьшении кода а возможности изменять контракты доменной модели или api не трогая view совсем

одна view использует несколько разных ViewModel, а это очень частая практика.

Вот как раз это обычно противоречит общепринятым практикам. В тех источниках, что попадались мне, советуют наоборот использовать только одну ViewModel во View.

Явный плюс mvvm даже не в уменьшении кода а возможности изменять контракты доменной модели или api не трогая view совсем

Как я уже ответил выше, любое изменение в интерфейсе модели потребует изменений в презентационном слое. Какая разница, будет это в классе Widget или в классе ViewModel?

Ну а с API напрямую View и не должна работать, поэтому на нее изменения в API и не повлияют.

Эээ.. А разве ViewModel - это не бизнес-логика? А Model - только данные?

Нет. Википедия:

Model–view–viewmodel (MVVM) is a software architectural pattern that facilitates the separation of the development of the graphical user interface (the view) – be it via a markup language or GUI code – from the development of the business logic or back-end logic (the model) so that the view is not dependent on any specific model platform.

А вот ViewModel – это, по сути, просто "an abstraction of the view exposing public properties and commands."

Ну так да, viewModel как раз этим самым и отделяет бизнес логику от view - тем, что она ее и содержит. Иначе какой в ней смысл, можно просто view/model обойтись

Нет. Model – это данные и бизнес-логика. View – это структура того, что пользователь видит на экране. А ViewModel – это посредник между view и бизнес-логикой / данными, а также абстракция этого самого view.

И что же этот посредник конретно делает?

Занимается UI-логикой. Связывается с моделью, запускает интеракторы бизнес-логики, получает данные, преобразует их в другую форму, чтобы view было удобнее их отображать.

и это не бизнес-логика?

Н-да?
In computer software, business logic or domain logic is the part of the program that encodes the real-world business rules that determine how data can be created, stored, and changed. It is contrasted with the remainder of the software that might be concerned with lower-level details of managing a database or displaying the user interface, system infrastructure, or generally connecting various parts of the program.
https://en.wikipedia.org/wiki/Business_logic

Как можно легко увидеть, то, что происходит во viewModel точно так же является бизнес логикой
Более того, нет никаких препятствий или правил против размещения БЛ во viewModels - достаточно представить, что она может быть разной для одного и того же набора данных, т.е., для одной и той же model
Многие источники так и прямо говорят, model - это просто данные. Они могут отбираться по разным БЛ правилам, но это вовсе не значит, что БЛ лежит исключительно в модели.
Так что вопрос в основном в том, как мы будем разделять БЛ, какая ее часть касается данных, и какая - их преобразований, т.к. view - это необязательно UI
Редуцируя viewModel до примитивного передатчика между Model и View, вы просто перегружаете Model ненужными ей правилами БЛ

https://en.wikipedia.org/wiki/Presentation_logic

Многие источники так и прямо говорят, model - это просто данные.

Какие, например?

view - это необязательно UI

А что еще?

Редуцируя viewModel до примитивного передатчика между Model и View, вы просто перегружаете Model ненужными ей правилами БЛ

Где я ее редуцирую? Я выше написал, что ViewModel "занимается UI-логикой. Связывается с моделью, запускает интеракторы бизнес-логики, получает данные, преобразует их в другую форму, чтобы view было удобнее их отображать."

Какие, например?

да хотя бы и википедия

Model refers either to a domain model, which represents real state content (an object-oriented approach), or to the data access layer, which represents content (a data-centric approach)
https://en.wikipedia.org/wiki/Model–view–viewmodel

А что еще?

Интерфейс к API, например

Я выше написал, что ViewModel "занимается UI-логикой

не, UI логикой занимается view, у viewModel и ссылки-то на view нет. Вы, наверное, с MVP путаете

A viewModel занимается трансформацией данных/команд, которые приходят от view или от model. Эта трансформация и есть БЛ, по крайней мере, часть ее

Model refers either to a domain model, which represents real state content (an object-oriented approach), or to the data access layer, which represents content (a data-centric approach)

Да, вот только domain model – это не просто данные, это данные и поведение, т.е. бизнес-логика.

Интерфейс к API, например

Интерфейс к интерфейсу? Это как?

не, UI логикой занимается view, у viewModel и ссылки-то на view нет. Вы, наверное, с MVP путаете

Ну вы хоть прочитайте ту статью с википедии, на которую кидаете ссылку, и выдержки из которой я уже несколько раз приводил: "In this respect, the viewmodel is more model than view, and handles most if not all of the view's display logic."

Эта трансформация и есть БЛ, по крайней мере, часть ее

Нет, это не бизнес-логика. Это просто "трансформация данных/команд, которые приходят от view или от model".

Да, вот только domain model – это не просто данные, это данные и поведение

там прям в цитате написано - or to the data access layer, which represents content (a data-centric approach)

Никто не говорит, что БЛ там вообще быть не должно. Может быть, может не быть, it's not carved in stone

Интерфейс к интерфейсу? Это как?

очень просто

У меня есть API, для пользователей этого API я выставлю наружу view, сиречь класс, который ничего не делает, просто принимает данные и команды и передает их во viewModel

В зависимости от конфигурации за этим view могут быть разные viewmodels

View - это абстракция, каким смыслом вы ее наполните, тем она и будет

In this respect, the viewmodel is more model than view, and handles most if not all of the view's display logic

ну нате вам другую ссылку
https://www.geeksforgeeks.org/difference-between-mvp-and-mvvm-architecture-pattern-in-android/

Нет, это не бизнес-логика. Это просто "трансформация данных/команд, которые приходят от view или от model".

Хы, а трансформация данных по определенным правилам - это не БЛ что ли?

Я говорю, что все эти mvc/mvp/mvvm - это абстракции, не ставящие жестких рамок. Хотите запихнуть всю БЛ в модел - ради бога. Хотите вo вьюмодел - с нашим удовольствием
Нету жестких правил, нету

Я говорю, что все эти mvc/mvp/mvvm - это абстракции, не ставящие жестких рамок. Хотите запихнуть всю БЛ в модел - ради бога. Хотите вo вьюмодел - с нашим удовольствием
Нету жестких правил, нету

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

Почему же бессмысленно, есть ведь и другие паттерны, при помощи которых можно оценить подход

Ваш, насколько я понял, предполагает в Model наличие data access layer & business logic layer; view+viewModel это presentation

И чем этот подход отличен от древнего майкрософтовского Document-View?
Не говоря уж о том, что Model получает чересчур много responsibilities, a viewModel, наоборот, служит довольно-таки бесполезным man in the middle

Подход же вида Model - DAL, viewModel - BL, view - presentation и проще, и более гибкий

Ваш, насколько я понял, предполагает в Model наличие data access layer & business logic layer; view+viewModel это presentation

Это не мой подход, это официальный подход. Посмотрите хотя бы первую картинку из этой статьи на википедии (которую я и в эту статью добавил) или почитайте статью на сайте Microsoft про MVVM.

Подход же вида Model - DAL, viewModel - BL, view - presentation и проще, и более гибкий

Он может быть сколь угодно проще и гибче, только это не MVVM.

Это не мой подход, это официальный подход

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

Он может быть сколь угодно проще и гибче, только это не MVVM

Не надо быть догматиком. Все современные паттерны и модели как раз и придуманы для гибкости, понятности и maintainability, а не для чего-то еще

Не говоря уж о том, что модель, в которой перемешана BL&DAL прямо нарушает сингл респонсибилити.

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

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

К чему я это? Если классов и слоёв станет меньше без ущерба для поддерживаемости и функциональности приложения, мы же все только выиграем. Так пусть же их и станет меньше!)) Вот.

Ну если судить по дате рождения, то я примерно такой же динозавр :) И тоже занимался бэкендом, фронтендом и прочее.

Если классов и слоёв станет меньше без ущерба для поддерживаемости и функциональности приложения, мы же все только выиграем. Так пусть же их и станет меньше!

Аминь!

Ну да возраста у меня только на мелкого падальщика, до серьёзного ящера ещё не дорос :-)

Ну или вот если вернуться к теме поста...

Давеча залез я пет-проект дописывать, и чувствую, что оооочень много скроллю в одном из виджетов, ещё и не понимаю, в каком участке кода нахожусь - просто IDE меня как-то выбрасывает сама, куда нужно, но я уже логически свои перемещения не контролирую. Нездоровая ситуация.

Ладно, думаю, код этого экрана слишком раздут, очевидно, что там недавно писали про ViewModel - дай попробую, вдруг поможет. Почитал про эту прослойку. Потом посмотрел на свой код... ага, так... тут у нас методы, которые представляют собой вообще сервис без состояния... ну и выносим их в сервис без состояния тогда. Так, а вот эта простыня оборачивается в три stateless-виджета. Чудненько. Что в итоге у меня в Stateful-виджете остаётся? Конструктор Scaffold и пару колбэков на кнопки в нём. И вызовы вышеупомянутых сервисов и виджетов.

Можно сказать, что в моём конкретном случае этот паттерн не подошел, и это будет абсолютно верным ответом.

Другой полярный пример, из моего же пет-проекта. Создавал я его из интереса на основе "шаблонного проекта", который разработчики Flutter в какой-то момент сделали в качестве примера "как надо делать". Ну и там для хранения настроек был сделан и экран настроек, контроллер для сохоранения настроек. который дёргает сервис для сохранения настроек... идеологически - правильно, но фактически у меня теперь в этих трёх классах гора копипаста, несчастная переменная гоняется из одного класса в другой и в третий, а потом в обратную сторону, и всё потому что чисто идеологически - это разные слои, абстракция от конкретной реализации и всё такое.... Слделало ли слепое следование методологии мою жизнь счастливее, а мой код - красивее и качественнее? Точно нет)) Только привело к едва одолимому желанию отрефакторить это всё, выбросив лишние слои и завернув всё это в один сервис. Пока терплю, потому что на работе приложения никак не сказывается, а количество настроект и так достигло своего предполагаемого лимита.

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

А можно примеры кода? Тут как-то всё расплывчато объяснено. Или на слишком упрощенных примерах, на которых преимущества ViewModel действительно не видны.

С примерами такая проблема – либо они действительно упрощенные, либо надо делать полноценный проект, и на нем показывать.

Sign up to leave a comment.

Articles