Всем привет! Представляю вам текстовую версию моего доклада на DartUP 2021 (сам доклад на английском можно найти здесь). Посвящен он довольно популярному архитектурному паттерну MVVM (он же Model-View-ViewModel), а конкретно той его части, что про ViewModel.
Паттерн этот весьма распространен в мире нативной Android-разработки – во многом благодаря тому, что является официальной рекомендацией Google. А поскольку многие Flutter-девелоперы попали в мир Flutter'а из нативного Android'а, то и подходы они склонны применять те же самые. Как говорится, работает – не трогай и ничего не меняй.
Но как по мне, в мире Flutter'а этот паттерн не то чтобы полезен, а скорее даже и вреден. И сейчас я буду вас в этом убеждать.
Пара слов обо мне. Помимо основной работы мобильным техлидом, я занимаюсь разного рода менторингом и консультированием стартапов по вопросам мобильной разработки в целом, и разработки на Flutter'е в частности. Так что у меня была возможность взглянуть на код за пределами уютного мирка нашей компании и несколько расширить кругозор.
История MVVM
В начале, как водится, было слово. Сказано оно было Microsoft'ом в 2005 году, чтобы описать паттерн для отделения GUI от бизнес-логики.
![](https://habrastorage.org/getpro/habr/upload_files/758/28b/46c/75828b46cd28d102066192d4b8b9a325.png)
Паттерн, как следует из этого слова, состоит из трех компонентов:
Model – то, что обычно относится к доменной модели. Про нее мы в рамках этой статьи говорить не будем.
View – структура и внешний вид того, что пользователь видит на экране. Ничем, по сути, не отличается от View из MVC или MVP.
ViewModel – главный герой этой статьи, абстракция над View и состояние данных из модели.
При этом View и ViewModel формируют презентационный слой, а Model – это уже слой бизнес-логики.
Основной идеей MVVM было буквально убрать весь GUI код из слоя View. Тогда UX-девелоперы смогли бы радостно вооружиться языком разметки и соорудить интерфейс мечты, оставив скучную реализацию логики другим разработчикам.
ViewModel и Android
Прошло 12 лет. На Google I/O 2017 команда разработчиков из Android Framework представила набор Architecture Components. Одним из этих компонентов, собственно, и была ViewModel. Решала она, по крайней мере отчасти, сугубо специфичные проблемы Android'а.
Дело в том, что Android сам управляет жизненным циклом UI-контроллеров, таких как Activity или Fragment. Фреймворк сам решает, когда надо уничтожить, а когда воссоздать UI-контроллер – в зависимости от определенных действий пользователя или в ответ на различные события устройства. Вы, как разработчик, никак повлиять на это не можете. Означает это, что данные в Activity или Fragment'е могут исчезнуть самым непредсказуемым образом и в самый неподходящий момент. Еще это означает, что за всякими фоновыми операциями тоже надо внимательно следить – вовремя освобождать ресурсы и создавать их заново, думать как избежать повторных запросов.
И тут на сцену выходит ViewModel, которая привязана к более лучшему жизненному циклу. Она остается в памяти, переживая взлеты и падения (т.е. уничтожения и создания) Activity, пока последняя окончательно не сгинет:
![](https://habrastorage.org/getpro/habr/upload_files/f31/99c/4f8/f3199c4f85a48840c7e759912ad03f35.png)
"Преимущества" MVVM
Почему в кавычках? Потому что никакие это не преимущества, во всяком случае, в мире Flutter'а.
Котлеты (верстка) – отдельно, мухи (код) – отдельно
Выше я уже говорил, что одной из основных задач изначального MVVM'а было убрать GUI-код из слоя View, и позволить писателям этих ваших интерфейсов использовать язык разметки.
Но во Flutter'е нет языка разметки (к счастью). У нас тут верстка – это и есть код! Берешь много-много виджетов и строишь себе UI, используя нормальную человеческую композицию. И уж точно тут нет каких-то отдельных верстальщиков.
Вот посмотрите на самый что ни на есть настоящий production-код. Просто описываем UI (причем исключительно в методе build
) и UI-логику.
class FailureScreen extends StatefulWidget {
const FailureScreen({Key? key, required this.reason}) : super(key: key);
final CheckOutFailureReason reason;
@override
_FailureScreenState createState() => _FailureScreenState();
}
class _FailureScreenState extends State<FailureScreen> {
@override
void initState() {
super.initState();
context.read<AnalyticsManager>().sendEvent(AnalyticsEvent.checkOutFailed(widget.reason));
}
void _onOkPressed() => context.read<SessionBloc>().add(const SessionEvent.initialized());
@override
Widget build(BuildContext context) => StepScreenWrapper(
child: OptimusStack(
mainAxisAlignment: OptimusStackAlignment.center,
spacing: OptimusStackSpacing.spacing300,
children: [
const OptimusSectionTitle(child: TranslatedText(TranslationKey.PleaseGoToReception)),
SizedBox(
width: 500,
child: OptimusParagraph(child: TranslatedText(widget.reason.message, textAlign: TextAlign.center)),
),
OptimusButton(
variant: OptimusButtonVariant.primary,
onPressed: _onOkPressed,
child: const TranslatedText(TranslationKey.OkGotIt),
),
],
),
);
}
UI-тесты
В мире нативного Android'а UI-тесты – это те самые благие намерения, которыми известно что и куда выложено.
![](https://habrastorage.org/getpro/habr/upload_files/4b2/1b6/73e/4b21b673efad5a3a82a3a29145ea0b0e.png)
Во-первых, тесты надо запускать на эмуляторе или реальном устройстве. В случае CI-машины нужно написать скрипты для запуска и остановки эмулятора, дождаться его загрузки, установить приложение, подумать, как и куда сохранять скриншоты.
Во-вторых, эти тесты намного медленнее, так что гонять сотни UI-тестов на каждый PR – это то еще удовольствие.
В-третьих, в случае всяких анимаций и асинхронных операций можно получить еще одну порцию проблем.
Есть, конечно, способы с этими проблемами бороться. Например, есть Firebase App Lab для более простого запуска тестов на эмуляторах и устройствах. Есть фреймворки типа Robolectric, которые позволяют запускать UI-тесты напрямую на JVM, без эмулятора. Но там свои ограничения и проблемы.
В этом случае вытащить UI-логику в отдельный класс действительно может быть хорошей идеей – тогда для этой логики можно написать unit-тесты и не заморачиваться со всей этой сложной инфрастуктурой.
Но в случае с Flutter'ом, UI-тесты запускать легко и приятно, как сказал бы Иешуа:
void main() {
testWidgets('MyWidget has a title and message', (WidgetTester tester) async {
await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));
final titleFinder = find.text('T');
final messageFinder = find.text('M');
expect(titleFinder, findsOneWidget);
expect(messageFinder, findsOneWidget);
});
}
Нормальный жизненный цикл
Я уже упоминал, что в Android'е жизненный цикл условного "экрана" довольно запутанный – например, система может пересоздать всю Activity только потому, что пользователь повернул телефон; все данные внутри Activity при этом пропадут. Тут ViewModel, которая привязана к "смысловому" жизненному циклу – это, простите за каламбур, жизненная необходимость.
![](https://habrastorage.org/getpro/habr/upload_files/46c/668/7ae/46c6687aed3628cd1537dbcceac65dfb.png)
Во Flutter'е у виджетов нормальный жизненный цикл из коробки. Посмотрите на эту диаграмму (она слегка упрощенная, но смысл передан) – никаких onStart
, onPause
, onResume
и прочих странных колбэков с неконсистентным поведением, всё просто и понятно:
![](https://habrastorage.org/getpro/habr/upload_files/8ed/162/68f/8ed16268faf0976efed9a39ce622a92e.png)
Причем для ViewModel нам бы всё равно понадобился какой-то минимальный набор колбэков – для инициализации, обновления и уничтожения экрана.
Single Responsibility Principle
Я часто слышу от разработчиков, мол, наличие UI-логики в виджете нарушает SRP (он же Single Responsibility Principle, он же принцип единой ответственности). "Класс должен делать одну вещь", – говорят мне в таких случаях, – "UI-логика и верстка в одном классе – это нарушение принципа".
На самом деле, это неправильная интерпретация принципа. Вот что сам Боб Мартин говорит по этому поводу в книге "Чистая архитектура":
Программист, услышав название принципа, легко можно предположить, что модуль должен делать только одну вещь.
Не поймите меня неправильно, такой принцип тоже есть. Функция должна делать одну, и только одну, вещь. Мы руководствуемся этим принципом на самом нижнем уровне, когда разбиваем большие функции на маленькие. Но это не один из принципов SOLID, это не SRP.
И дальше он предлагает улучшенную формулировку этого принципа:
Модуль должен отвечать перед одним, и только одним, актором.
Но в большинстве случаев кусок UI и так отвечает перед одним актором – это UI/UX дизайнер. SRP не имеет никакого отношения к отделению UI-логики от UI-элементов.
Переиспользование ViewModel
Говоря о переиспользовании, обычно подразумевают два сценария.
Первый – это когда вы хотите сделать интерфейс, который совершенно не зависит от фреймворка. Например, кросс-платформенное приложение может иметь общую бизнес-логику и общие ViewModel'и, а реальные компоненты интерфейса для каждой платформы (Android и iOS) будут свои. Такой подход, по крайней мере в теории, мог бы иметь смысл для KMM (Kotlin Multiplatform Mobile) – там интерфейс действительно пишется отдельно под каждую платформу. В этом случае можно выделить ViewModel как абстракцию над слоем View, где задать всю логику, а непосредственную реализацию компонентов сделать специфичной для каждой платформы.
![](https://habrastorage.org/getpro/habr/upload_files/620/955/c0b/620955c0b8e677b3f851c87c06a91a78.png)
Но во Flutter'е смысла в этом нет вообще – UI код здесь общий для всех платформ, как и логика.
Второй сценарий – это использование одной ViewModel для разных View:
![](https://habrastorage.org/getpro/habr/upload_files/43b/3ca/687/43b3ca68716f728eee72c71385f23500.png)
На моей практике это довольно редкая ситуация – обычно либо переиспользуешь весь компонент (т.е. связку View + ViewModel), либо дробишь ViewModel на несколько независимых. Но если вдруг надо переиспользовать какие-то данные или логику, то это можно прекрасно сделать и без паттерна ViewModel – мы поговорим от этом чуть позже.
Недостатки MVVM
А вот недостатки, в отличие от "достоинств", вполне реальны и ощутимы.
Сильное зацепление (tight coupling)
Есть смысл разделять один модуль на несколько под-модулей в том случае, когда у них разная зона ответственности. В идеале, изменения в одном модуле никак не должны влиять на другой модуль.
На практике, однако, View и ViewModel сильно cцеплены (coupled), а изменения в требованиях к UI приводят к изменениям в обоих классах.
Представьте, что у нас есть экран профиля с кнопкой Log out
. В первой итерации всё просто: нажимаем на кнопку – заканчивается сессия. В следующем спринте к вам подходит дизайнер и говорит: "Слушай, надо добавить диалог с подтверждением". Изменения придется вносить как во ViewModel (изменяется логика работы), так и во View (он теперь должен реагировать на запрос диалога).
Сложности с тестированием
Если у нас View и ViewModel – это разные классы, нам понадобится по крайней мере два типа тестов:
Для ViewModel – убедиться, что она дергает правильные методы бизнес-логики и правильно преобразует данные для презентации.
Для View – проверить правильность отображения данных презентационной модели. А выше мы уже обсуждали, что в нативном Андроиде UI-тесты – это очень больно. Так что, скорее всего, это часть кода будет не очень хорошо покрыта тестами.
В случае с виджетами, нам нужен тест виджета. И смысла в этом гораздо больше, так как мы можем протестировать ожидаемый результат, а не внутреннюю логику.
Вы же помните правило для написания хороших комментариев? Не комментируйте, как работает код, пишите, почему это так сделано. То же и с тестами: не надо тестировать, как работает система, проверяйте, выдает ли она ожидаемый результат.
В случае с диалогом подтверждения – мне не важно, вызывает ли нажатие на кнопку метод onLogoutButtonClicked
. Мне нужно знать, что если я нажму на кнопку, появится диалог.
Повторюсь, в нативном Андроиде с этим всё сложно, во Flutter'е – гораздо проще.
Больше кода
Естественно, больше сущностей – больше кода (как правило). Само по себе это не страшно, многие хорошие практики вообще-то увеличивают количество кода. Но это имеет смысл лишь в том случае, когда большее количество кода компенсируется лучшей читаемостью или поддерживаемостью. Иначе это просто больше кода.
Что делать?
Итак, мы разобрались с тем, что преимущества не очень-то значимые, а недостатки вполне ощутимые. А делать-то с этим что?
Keep It Simple And Straightforward
Прежде всего, следовать самому, на мой взгляд, важному принципу в разработке (а то и в жизни). Существуют разные вариации этой фразы, я предпочитаю говорить: "Keep it simple and straightforward" – "Делай просто и прямолинейно". Или же, следуя бритве Оккама: "Не следует множить сущее без необходимости".
Применительно к нашей ситуации: если можно добиться такого же уровня читаемости и поддерживаемости уже имеющимися инструментами, не надо вводить новые сущности типа ViewModel.
Какие же это инструменты?
Всё является виджетом
Вы наверняка много раз слышали эту фразу: "Во Flutter'е всё является виджетом". Не надо, конечно, воспринимать эту фразу слишком буквально – помимо виджетов во Flutter'е есть на что посмотреть. Но доля правды тут определенно имеется.
Давайте условно сгруппируем виджеты на 3 категории. Я не предлагаю превращать эти категории в базовые классы, или вообще как-то применять названия этих групп в коде – они нужны только для описания ролей, которые могут играть виджеты.
Чистый виджет
По аналогии с чистой функцией из функционального программирования, это виджет, который зависит только от входящих данных. Обычно строится на базе StatelessWidget
, но это может быть и StatefulWidget
– например чекбокс со своим внутренним состоянием.
Ключевой момент здесь – не наличие или отсутствие внутреннего состояния, а отсутствие любых зависимостей от бизнес-логики.
Эти виджеты – как маленькие переиспользуемые компоненты, они же являются первыми кандидатами на золотые тесты UI. Поскольку они никак не зависят от бизнес-логики, в них, в первую очередь, нас интересует то, как они выглядят в зависимости от разных параметров.
Как понять, что виджет относится к этой категории? Ориентируйтесь на название – оно не должно быть связано с доменом вашего приложения – например, Checkbox
, Button
, RoundAvatar
– т.е. компоненты, которые могут встретиться в любом интерфейсе.
Виджет с данными
Полная противоположность предыдущему. Он вообще не использует видимых компонентов – он управляет данными.
Этот виджет ближе всех к концепту ViewModel из MVVM, поскольку он взаимодействует с интеракторами из слоя бизнес-логики и репозиториями, передает данные вниз по дереву виджетов.
Обычно такой виджет представляет из себя Builder
или StatefulBuilder
– если нужно передать данные непосредственно вложенному виджету. Если данные надо пробросить куда-то глубоко, то удобно использовать InheritedWidget
или Provider
.
Главное отличие от ViewModel (кроме того, что технически это всё еще виджет) в том, что ради одного виджета нет смысла вводить разделение на ViewWidget
и ViewModelWidget
. Польза от него начинается, когда данные нужно расшарить между несколькими виджетами.
Виджет-компонент
Нечто среднее между первым и вторым типами. Когда нет смысла вводить полноценный виджет с данными (например, бизнес-логика нужна, но только локально), можно смело цепляться к репозиторию или методам бизнес-логики и использовать эти данные внутри виджета.
Важное уточнение: я не агитирую за реализацию бизнес-логики в виджетах – нет, это всё еще презентационный слой. Я говорю только о потреблении данных из репозиториев и вызове методов из слоя бизнес-логики.
BusinessLogic-Widget
С учетом всего вышесказанного я предлагаю более простой паттерн. Можем назвать его BusinessLogic-Widget.
BusinessLogic – это слой, который отвечает за логику приложения; логику, которая не зависит от UI, ни в коде, ни по смыслу. Это означает, что такие вещи, как роутинг или диалоги подтверждения, не принадлежат этому слою. Как я обычно говорю, лучший способ определить слой для куска логики – это представить, что надо заменить GUI на CLI: если код после этого теряет смысл, то скорее всего он и не должен являться частью бизнес-логики.
Конкретная реализация этого слоя не важна – это может быть BLoC, Redux, просто Use Case Interactor из чистой архитектуры и т.д. Важно то, что этот слой занимается обработкой и предоставлением данных, не привязанных к UI.
Слой Widget отвечает за получение этих данных и представление их пользователю. Мобильные приложения в целом чаще всего отталкиваются от концепции пользователя, или же от UI, так что этот слой лучше знает, какая нужна область видимости у данных (обычно это некое под-дерево), и когда надо вызвать метод из бизнес-логики. Поскольку у виджетов в любом случае есть доступ к контексту, многие часто задаваемые вопросы просто не имеют смысла, например: "Как осуществлять навигацию без доступа к BuildContext
?" Да никак – это ответственность UI, а там есть доступ к BuildContext
.
Давайте рассмотрим один экран из нашего приложения:
![](https://habrastorage.org/getpro/habr/upload_files/543/5c8/85e/5435c885e0f64b7dbf3fe2421c3c7678.png)
Не обращайте внимания на верхнюю панель, нас интересует только основное содержимое экрана (то, что на сером фоне). Можно выделить несколько компонентов: текст "Добро пожаловать..." с названием отеля, кнопки Check in
/ Check out
и номер версии в правом нижнем углу.
В простейшем случае (на самом деле, это очень близко к нашей фактической реализации) у нас может быть более или менее плоская структура виджетов. Технически, каждый виджет на этом экране принадлежит к той группе, которую мы условно назвали "Чистый виджет", сам экран при этом выступает в роли провайдера данных:
![](https://habrastorage.org/getpro/habr/upload_files/8c5/582/4ec/8c55824ec33de7ced2d066ab5635f535.png)
Текст сообщения "Добро пожаловать..." содержит название отеля, которое берется из модели
Session
(часть бизнес-логики).Кнопки
Check In
иCheck Out
получают колбэки, осуществляющие переход на другие экраны; кнопкаCheck Out
при этом еще отправляет событие в аналитику –AnalyticsManager
является частью бизнес-логики.Текст версии состоит из, собственно, номера версии и номера билда, эти данные берутся из
PackageInfo
.Сам экран через
BlocListener
подключается кUpdateManager
'у и переходит на другой экран, когда получает уведомление о наличии новой версии.
При этом ничто не мешает нам выделить какие-то части экрана в независимые виджеты – они уже ведут себя более или менее независимо и получают данные из разных источников.
В этом случае мы получим такую структуру:
![](https://habrastorage.org/getpro/habr/upload_files/39b/340/825/39b340825d71253ddbf9cf28b8c08ef8.png)
Сам экран больше не содержит логики вообще. Он просто состоит из нескольких "умных" виджетов.
UpdateWatcher
, наоборот, представляет из себя виджет-наблюдатель – в нем нет никаких видимых компонентов, он просто получает события о наличии новой версии и соответствующим образом их обрабатывает (переходит на другой экран).Другие виджеты знают, какие данные им нужны, как их получить и как обработать.
Мне кажется, это более поддерживаемый и расширяемый подход: вместо разделения классов по их функции (ViewModel занимается обработкой данных, а View – внешним видом), мы получаем много неделимых (в том плане, что делить их дальше уже нет смысла) компонентов, каждый из которых выполняет свою задачу.
Flutter позволяет легко и эффективно управляться с огромным количеством виджетов. Иногда много маленьких виджетов – это даже лучше с точки зрения производительности, например, когда можно часть под-дерева завернуть в виджет-константу.
Подводя итог: не используйте во Flutter'е ViewModel, не множьте сущности без необходимости и пользуйтесь встроенными средствами.
Комментарии (54)
allswell
19.12.2021 17:00-1Соглашусь, нынче товарищи любят все переинженирить, изучить 1 паттерн и использовать везде, где не нужно, лишь бы что-то "модное" для галочки затащить и щеголять красивыми аббревиатурами при описании проекта, не понимая для какой конкретной цели использует и какие плюсы и минусы это приносит
mitrych
19.12.2021 17:33+1Автор показывает, скорее, теоретическое владение вопросом и делает на его основе смелые выводы. Почему-то среди преимуществ MVVM не значится уменьшение количества строк в файлах View и ViewModel за счет их разделения, удобство командной работы над проектом за счет формализации правил архитектуры. Не отмечено также, что преимущество "переиспользование ViewModel" редко используется на практике. Фактически ViewModel имеет одно главное преимущество ради которого его используют - разделение бизнес-логики и интерфейса. Это очень важно при работе со сложными проектами. Их код удается поддерживать в состоянии, когда его можно редактировать, не добавляя сложно устраняемые баги.
BTW UI во флаттере можно использовать для всех платформ только оооочень редко. Интерфейсы iOS и Android сильно отличаются, привычки пользователей тоже. Чаще всего, единственное преимущество флаттера в том, что он позволяет одному программисту писать приложение для разных платформ. Время он может немного сэкономить, но тоже не всегда. Флаттер хорошо подходит чтобы запилить прототип, когда в команде есть человек, знакомый с ним.
BLoC на флаттере хорошо подходит для реализации похожей на MVVM архитектуры. На мой взгляд, не стоит делать "все-в-кучу-виджеты" только потому что так тоже можно.
Говоря другими словами, если хотите писать хороший код, который можно долгое время поддерживать, разделяйте интерфейс и логику. Хотите хорошенько прочувствовать, почему разделение бизнес-логики и интерфейса это хорошая идея, даже когда не очень удобно - валите все в кучу, потом напишете статью про трудный рефакторинг.
ookami_kb Автор
19.12.2021 17:55+1Почему-то среди преимуществ MVVM не значится уменьшение количества строк в файлах View и ViewModel за счет их разделения
Само по себе уменьшение количества строк в файле – так себе преимущество, если это достигается введением дополнительной сущности и усложнением связей.
удобство командной работы над проектом за счет формализации правил архитектуры
Автор всецело "за" формализацию правил архитектуры, это не преимущество конкретно MVVM, это преимущество любой формализации.
Не отмечено также, что преимущество "переиспользование ViewModel" редко используется на практике.
Отмечено, прочитайте внимательно пункт "Переиспользование ViewModel".
Фактически ViewModel имеет одно главное преимущество ради которого его используют - разделение бизнес-логики и интерфейса.
Нет, к разделению бизнес-логики и интерфейса ViewModel не имеет никакого отношения. Бизнес-логикой в MVVM вообще занимается слой M. ViewModel – это отделение UI-логики от UI.
На мой взгляд, не стоит делать "все-в-кучу-виджеты" только потому что так тоже можно.
Я нигде не упоминал, что нужно делать "все-в-кучу-виджеты". Совсем даже наоборот. Прочитайте внимательно пункт "Виджет-компонент".
Если хотите писать хороший код, который можно долгое время поддерживать, разделяйте интерфейс и логику. Хотите хорошенько прочувствовать, почему разделение бизнес-логики и интерфейса это хорошая идея, даже когда не очень удобно - валите все в кучу, потом напишете статью про трудный рефакторинг.
Вы точно прочитали статью? Я ж всю дорогу твержу о том, что есть бизнес-логика, UI-логика и UI. Так вот, бизнес-логику можно и нужно отделять от интерфейса. UI-логику от UI отделять не надо – а именно на это и направлена VM.
IngweLand
20.12.2021 00:38+1Нет, к разделению бизнес-логики и интерфейса 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.
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.
ookami_kb Автор
20.12.2021 00:43Если мы берем паттерн MVVM, то M – это слой бизнес-логики, а V-VM – это презентационный слой (посмотрите на схему в начале статьи). Да, VM в этом случае будет дергать методы из бизнес-логики и преобразовывать ее данные.
Если мы не разделяем V и VM, у нас все еще остается слой бизнес-логики, который занимается бизнес-логикой. В условном паттерне BusinessLogic-Widget, точно так же разделена бизнес-логика и интерфейс.
Т.е. бизнес-логика и так отделена, хоть есть VM, хоть нет ее. Именно это я подразумеваю под "не имеет никакого отношения". Не нужен паттерн VM, чтобы отделить бизнес-логику от презентации.
IngweLand
20.12.2021 01:09+1Отделение бизнес-логики от вью - это не разнесение всего этого по разным классам. Отделение, в данном случае - это когда V и М не связаны напрямую. Именно такой вот непрямой связкой и занимается вьюмодель. Если вы объедините вью и вбюмодель в один класс, то у вас вью будет отвечать за коммуникацию с моделью.
В данном случае, я не спорю с вами о главном выводе, просто потому, что я не достаточно хорошо знаю Flutter. Возможно каки-то особенности именно этого фреймворка приводят к тому, что MVVM не подходит. Но откровенно говоря, приведенные вами примеры этого не описывают.
Мне кажется. что вы основываете свои выводы на утверждении, что VM - это просто UI-логика. А это не так. Наличие UI-логики во вьюмодели, не означает, что там нет логики по коммуникации с моделью (что приводит нас к выводу, что это не исключительно UI-логика).
ookami_kb Автор
20.12.2021 01:58Вот именно про коммуникацию с бизнес-логикой в виджете я и говорю. Я же прямым текстом об этом пишу:
Важное уточнение: я не агитирую за реализацию бизнес-логики в виджетах – нет, это всё еще презентационный слой. Я говорю только о потреблении данных из репозиториев и вызове методов из слоя бизнес-логики.
IngweLand
20.12.2021 02:10+1Собсвенно я тоже нигде не говорил за реализацию бизнес-логики в слое презентации. Эта коммуникационная прослойка - это и есть вьюмодель. И именно эта прослойка отделяет вью от модели. Теперь у вас ваша вью отвечает не только за то, как данные выглядят на экране, но и за то, чтобы их запросить, форматировать и прочее.
ookami_kb Автор
20.12.2021 02:25Теперь у вас ваша вью отвечает не только за то, как данные выглядят на экране, но и за то, чтобы их запросить, форматировать и прочее.
Вот мы и пришли к тому, о чем я говорю всю статью. Только не View, а Widget.
Если у вас сложное форматирование, никто не мешает вынести это в отдельную функцию, и написать на нее юнит-тест.
Если у вас сложный UI, никто не мешает вынести его в чистый виджет, которому все данные передаст другой виджет, который свяжется с бизнес-логикой.
Но это все ситуационное выделение классов и методов там, где это нужно. Заводить ViewModel и говорить, что вся коммуникация с бизнес-логикой осуществляется только через нее, вся UI-логика крутится только в ней, а Widget – это тупой рендер приходящих данных – это совершенно ненужное переусложнение в случае Flutter'а (отчасти потому, что с усложнением условий и требований абстрактная ViewModel начинает все больше и больше напоминать StatefulWidget).
proninyaroslav
21.12.2021 23:39BTW UI во флаттере можно использовать для всех платформ только оооочень
редко. Интерфейсы iOS и Android сильно отличаются, привычки
пользователей тоже.Не соглашусь. Из коробки Flutter имеет как Material, так и Cupertino виджеты. Так что при желании можно делать UI для Android и iOS, которые будут выглядеть и работать нативно, не имея разных спецов под каждую платформу.
alexandrim
19.12.2021 19:30+2Кирилл, спасибо за доклад и отдельно за его расшифровку!
Используя Provider, я пришёл к похожему подходу на небольшом проекте, который ещё не вышел из стадии MVP. Мне было бы спокойнее, зная что можно будет масштабировать эту же кодовую базу, а не переписывать всё заново. Пока замена зависимостей, включая Provider не выглядит болезненной, но сомнения остаются, вероятно, в силу инерции мышления.
Вероятно, для многих будет полезно, если вы обозначите, какого масштаба проекты построены на описываемых вами принципах.
Спасибо!ookami_kb Автор
19.12.2021 20:02+1Спасибо!
Сами приложения относительно небольшие, но это не разовые проекты – одно из них уже 2 года в продакшене, и оно постоянно поддерживается и обновляется. Другое мы закончили переписывать с нативного Android на Flutter около полугода назад, оно тоже все еще развивается.
В разное время над этими приложениями работало от 1 до 4 разработчиков, так что, опять же, это не one-man-show, и код должен быть понятным и поддерживаемым.
С масштабированием никаких особых проблем я не наблюдаю – напротив, независимые компоненты и меньшее количество сущностей позволяют быстрее разобраться в коде.
alexandrim
19.12.2021 20:11Это интересно!
Укладываются ли на ваш взгляд пакеты вроде graphql_flutter в такую парадигму?
Возможно этот пакет именно её и реализует? Однако, границы его применимости не вполне очевидны, не смотря на кейс авторов пакета (у них есть коммерческое приложение на его основе). Мне не удалось найти их дао, позволяющее не споткнуться на этом пути.
Знакомы ли вам примеры открытых репозиториев, придерживающихся похожего на ваш подход? Это могло бы быть полезным в дискуссии с коллегами, придерживающимися одного подхода для любых проектов.
Благодарю!ookami_kb Автор
19.12.2021 20:32С
graphql_flutter
не работал, поэтому ничего не могу сказать, к сожалению. На первый взгляд – скорее нет, чем да; все-такиgraphql
– это слой данных, а слою данных, тем более отдельной библиотеке для его реализации, нечего делать в виджетах. Я бы, скорее всего, взял чистый пакетgraphql
и сам бы интегрировал, куда мне надо. Но это, опять же, просто судя по первому впечатлению, может, я не совсем понял их концепцию.Тот же
flutter_bloc
, например, это, по большей части,bloc
+provider
, так что тут ничего не нарушается – BLoC'и отдельно и независимо от виджетов и Flutter'а в целом, иBlocProvider
/BlocBuilder
для встраивания их в дерево виджетов.Открытых репозиториев тоже не знаю, но надеюсь, что в более или менее ближайшем будущем получится открыть код одного из проектов, в котором я участвую (но каких-то конкретных сроков пока нет).
alexandrim
19.12.2021 21:01Авторы этих пакетов тоже основывают свои взгляды на своей же коммерческой практике и имеют смелость предлагать своё решение сообществу. И судя по pub.dev и github, сообщество активно откликнулось.
Понимаю, что их подход выглядит весьма радикально. Но более ли радикально, чем Flutter way по отношению к более традиционным подходам?
Всё же хочется расширять свой инструментарий, не только зрелыми, но и перспективными подходами.
Сейчас я использую чистый пакет graphql, а на использование graphql_flutter не решился и задаюсь вопросом, а не стал ли я динозавром.
Хотя опыт коллег-нативщиков с Apollo показывает, что всё состояние вполне может жить в кеше клиента graphql и тогда сущностей становится ещё меньше, функционал локализуется ещё лучше и для работы над приложением в команде достаточно понимать GraphQL API и свою часть не маленького приложения, чтобы эффективно включиться в разработку.
Наверное, это похоже на микрофронтенд.
Благодарю!ookami_kb Автор
19.12.2021 22:25Я не считаю ни мой подход, ни подход авторов graphql_flutter чем-то радикальным или незрелым. Принципу KISS лет 50, бритве Оккама и того больше.
Всё зависит от требований. Любой дополнительный уровень абстракции – это усложнение архитектуры, поэтому всегда надо думать, ради чего будет это усложнение.
Если мобильное приложение представляет из себя рендер данных, полученных от graphql (пусть даже и сложный с точки зрения UI), то такой подход (как у авторов graphql_flutter) вполне себе оправдан. Зачем вводить дополнительный слой, если там не будет никакой полезной работы?
У нас в приложениях довольно много бизнес-логики, т.е. нельзя просто взять данные от бэкенда и показать их, с ними надо много чего сделать. Так что у нас данные из бэкенда отделены от виджетов и с ними работает бизнес-слой.
Поэтому, мой подход (про который я говорю в статье) в более практическом смысле отличается от подхода graphql_flutter. Но если говорить в более широком смысле – да, и там, и тут затрагивается принцип KISS.
А вот в случае с ViewModel и Flutter, на мой взгляд, идет усложнение без особых преимуществ, и принципу KISS это противоречит. Об этом и статья.
alexandrim
19.12.2021 22:56+2Ни в коем случае не хотел назвать такой подход незрелым. Не самым удачным способом назвал своё ощущение, возникающее, вероятно, от дискуссии с воинствующими адептами от Clean Architecture. Бывает не просто отрешиться от эмоций и влияния авторитета.
Ваш аргумент понятен и вопросов не вызывает. Но по-прежнему есть желание посмотреть на хотя бы средних размеров проект, желательно, не на свой :)
OkunevPY
20.12.2021 16:21+1Чтобы так громко рассуждать про KISS НАЧНИТЕ ПИСАТЬ ПРОСТО. Выкиньте свой flutter и напишите приложение просто и влоб.
Прям вот без каких либо фреймвороков, обёрток и тому подобного. Если Ios то Swift в помощь, если андроид то ява, докажите свою приверженнность подходу, пишите простенько и нативненько.
ookami_kb Автор
20.12.2021 23:28А почему это без Flutter'а писать будет проще? Мне вот проще написать один раз, а не два. И поддерживать приложение на фреймворке с изначально декларативном подходом к UI мне тоже проще.
KISS ведь не про отсутствие любых абстракций. Он про отсутствие ненужных абстракций.
Devoter
20.12.2021 05:56-1Правильно ли я понимаю, что вы предлагаете вместо слабой связности между представлением и моделью использовать сильную связность? Если так, то позволю себе с вами не согласиться. Для небольших проектов ваш подход может быть даже более предпочтительным, как раз ввиду малого количества абстракций, но вот любой достаточно сложный проект превратится в неподдерживаемый, ведь:
Теперь невозможно без боли реализовать отличающиеся интерфейсы для разных платформ с учётом их специфики, просто написав несколько различных представлений одной и той же части интерфейса
Любое изменение в интерфейсе модели будет требовать изменения ваших умных виджетов, ведь они отвечают не только за представление, но и за получение данных из модели
Не раз замечал, как разработчики начинают путать логику UI и логику взаимодействия с моделью. Как по мне, логику UI (фокус, цветовые акценты, эффекты и т.д.) нужно оставлять в представлении, все остальное должно быть внедрено в представление изве. Тогда изменение модели и представления могут происходить независимо (до определенного момента, конечно).
ookami_kb Автор
21.12.2021 21:44+1Теперь невозможно без боли реализовать отличающиеся интерфейсы для разных платформ с учётом их специфики, просто написав несколько различных представлений одной и той же части интерфейса
Возможно. Никто не мешает выделить абстрактный платформо-независимый виджет и передать в него параметры сверху. Этот самый виджет может вызывать уже специфичные виджеты для конкретной платформы.
Любое изменение в интерфейсе модели будет требовать изменения ваших умных виджетов, ведь они отвечают не только за представление, но и за получение данных из модели
Любое изменение в интерфейсе модели потребует изменений в презентационном слое. Какая разница, будет это в классе Widget или в классе ViewModel?
Не раз замечал, как разработчики начинают путать логику UI и логику взаимодействия с моделью. Как по мне, логику UI (фокус, цветовые акценты, эффекты и т.д.) нужно оставлять в представлении, все остальное должно быть внедрено в представление изве. Тогда изменение модели и представления могут происходить независимо (до определенного момента, конечно).
Что именно вы подразумеваете под логикой взаимодействия с моделью? Знание о том, какой метод из слоя бизнес-логики дернуть (aka какой запустить Use Case Interactor)?
Ну а с тем, что логику UI надо оставлять в представлении (если под этим подразумевать Widget), я как раз и не спорю.
kraidiky
20.12.2021 10:21Я вообще не из Flutter'а но мои пять копеек:
— Хорошо когда для всего есть модель, подпадающая под правила работы с моделью, но есть информация, которая для правильного отображения нужна, но бизнес-логику совершенно бессмысленно замусоривает, типа того, какой из подпунктов выпадающего списка выделен в данный момент. VM Место как раз для такой информации, а до контроллеров и модели бизнеслогики дойдёт только информация о том, что выделен другой элемент, или даже ещё более бизнесовая.
И тут дело даже не в том, что хранить это в VM плохо, а в том, что больше её девать то некуда, только внутрь View как делают все когда пишут в первый раз, а вот это уже точно плохо-паттерн.ookami_kb Автор
21.12.2021 21:50Ну вот я пишу сильно не в первый раз, и писал раньше сильно по-разному. На этот путь я встал совершенно осознанно.
И тут дело даже не в том, что хранить это в VM плохо, а в том, что больше её девать то некуда.
Девать ее можно и, по моему мнению, даже нужно туда, где этой UI логике самое место – в UI слой. Я не говорю про то, что этот должен быть один экземпляр класса, разбивать на мелкие компоненты нужно. Но выделять отдельный слой ViewModel, который ничего не знает про View, но при этом занимается логикой этого View – это как-то странно. Опять же, зачем это нужно было в других фреймворках – понятно, но во Flutter'е таких проблем нет.
OkunevPY
20.12.2021 13:06-1Ну уж тогда и мои 5 копеек.
Смысл и суть самого паттерна MVVM оторвать данные от view, а саму view от деталей бизнес логики. Не затронут аспект когда одна view использует несколько разных ViewModel, а это очень частая практика.
Явный плюс mvvm даже не в уменьшении кода а возможности изменять контракты доменной модели или api не трогая view совсем
ookami_kb Автор
21.12.2021 21:54одна view использует несколько разных ViewModel, а это очень частая практика.
Вот как раз это обычно противоречит общепринятым практикам. В тех источниках, что попадались мне, советуют наоборот использовать только одну ViewModel во View.
Явный плюс mvvm даже не в уменьшении кода а возможности изменять контракты доменной модели или api не трогая view совсем
Как я уже ответил выше, любое изменение в интерфейсе модели потребует изменений в презентационном слое. Какая разница, будет это в классе Widget или в классе ViewModel?
Ну а с API напрямую View и не должна работать, поэтому на нее изменения в API и не повлияют.
ldss
20.12.2021 19:19Эээ.. А разве ViewModel - это не бизнес-логика? А Model - только данные?
ookami_kb Автор
21.12.2021 21:57Нет. Википедия:
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."
ASGAlex
21.12.2021 11:14+1Я вообще из бэкенда, ещё и динозавр немного, поэтому моё мнение тут ну разве что для статистики.
В целом, глядя на мобильную и фронтенд-разработку, возникает чувство, что как-то перебор там с разными паттернами и подходами... точнее не так. Мучает ощущение, что "паттернов" и "подходов" стало сильно больше, чем собственно, решения конкретной клиентской задачи. Естественно, они не просто так появились, и призваны решать какие-то насущные задачи в конечном итоге, ещё б я этого не понимал. Но в самом деле, когда приходишь в проект или начинаешь новый - всегда ведь хочется, чтобы задачи решались там просто и очевидно, без ритуальных танцев вокруг идеологически правильной архитектуры.
К чему я это? Если классов и слоёв станет меньше без ущерба для поддерживаемости и функциональности приложения, мы же все только выиграем. Так пусть же их и станет меньше!)) Вот.
ookami_kb Автор
21.12.2021 22:00Ну если судить по дате рождения, то я примерно такой же динозавр :) И тоже занимался бэкендом, фронтендом и прочее.
Если классов и слоёв станет меньше без ущерба для поддерживаемости и функциональности приложения, мы же все только выиграем. Так пусть же их и станет меньше!
Аминь!
ASGAlex
21.12.2021 22:40Ну да возраста у меня только на мелкого падальщика, до серьёзного ящера ещё не дорос :-)
VadimZhuk0V
Эм, каким образом добовленте деалога для логаута изменяет viewModel?
Собственно flutter_bloc это и есть по сути viewModel особенно если использовать cubit
ookami_kb Автор
Что делать при нажатии на кнопку – заканчивать сессию сразу или показать диалог – это логика UI, не так ли? Если мы договариваемся, что вся логика UI содержится во ViewModel, то, меняя эту логику, нам надо изменить ViewModel.
Нет, BLoC – это паттерн для имплементации бизнес-логики (на что, собственно, намекает название – Business Logic Component). Не надо из него делать ViewModel.
jonie
не так.
не придётся. (далее код условен): MVVM пришёл (ну или точнее "популарялизивался") с C# (WPF) в общем случае в vm передаётся некий IDialogManager (dlgMan), у которого vm в callback (ICommand если речь про WPF) делается: 1) ставится проперти "IsLogoutButtonEnabled=false" 2) делается if(dlgMan.ShowConfirmation()) { sessionMan.DoBlabla(); navigationMan.DoBlabla() ... }
ookami_kb Автор
А что это тогда?
Вы же сами дальше пишете: "делается if(dlgMan.ShowConfirmation())" – и почему же это не изменение во ViewModel?
mitrych
А что такого страшного, что приходится менять View и ViewModel одновременно? Суть не в том, чтобы править один файл, суть в том, чтобы поддерживать порядок в проекте в течение долгого времени. MVVM не экономит время, он не экономит и количество строк суммарно, зато каждый файл сильно меньше суммарного.
ookami_kb Автор
Сильно страшного – ничего. Но когда изменение одного требования приводят к изменениям в нескольких классов, часто это показатель того, что эти классы тесно сцеплены. Об этом я тоже пишу в статье.
Почему tight coupling – это плохо, есть куча материала.
jonie
да нет изменений в view когда вы хотите вместо "по нажатию кнопки сделать навигацию" сделать "по нажатию отобразить диалог и если юзер согласен сделать навигацию и погасить сессию". Нету этих изменений. Притом неважно сколько там этих "кнопок" на вью было (а их может быть чуток побольше чем одна)...
ookami_kb Автор
До изменений у вас кнопка
Log out
, нажатие на которую вызываетvm.onLogOutClicked()
После изменений – View получает информацию
isDialogVisible
и рендерит диалог, воViewModel
отсылаютсяonDialogCanceled()
иonDialogConfirmed()
– если же у вас, конечно, классический MVVM, в котором View рендерит интерфейс на основании данных из ViewModel.Deosis
Тут два варианта разрулить ситуацию:
view не сразу вызывает
vm.onLogOutClicked(), а сначала вызывает диалог, и только при подтверждении диалога передает сигнал во ViewModel
ViewModel, в методе
onLogOutClicked запрашивает отрисовку диалога и ждет подтверждения, и только потом продолжает бизнес логику.
В обоих вариантах будет менятся только один класс.
jonie
ну все методы типа "dlgMan.ShowConfirmation()" не блокирующие (не зря завезли async-await).
а что касается "View получает информацию
isDialogVisible
и рендерит диалог", то обыкновенно диалоги (а точнее место под их размещение в разметке если угодно), блокирующие весь "ввод" (модалка), заранее существуют в корневом view по-типу "портала, где этот диалог отрисовывается", собственно с этим и взаимодействует dlgMan - он-то знает как сделать так, чтобы диалог "отобразился".Диалоги они в целом не самый хороший пример и всегда с "хаками" делались (например на angular - там нужно указать portal где будет жить динамическая разметка диалогов). А если без хаков, и сам диалог надо рисовать в том же вью - то, конечно, там вью менять надо - ибо откуда б взяться "разметке".
ookami_kb Автор
Первый вариант – да, это то, что я предлагаю. Только в рамках модели MVVM получается, что UI-логика утекает из ViewModel во View.
Второй вариант – тогда ViewModel начинает подозрительно напоминать Presenter из MVP.
jonie
потому что так делать не надо и вы заблуждаетесь.
ничуть. в 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") }".
ookami_kb Автор
Это ваше мнение.
Я не про
loginBtnEnabled
– с этим и так всё понятно. Я как раз проdlgMan.Ask()
– диалог – это точно такая же часть UI, как кнопка, label и т.д. Почему это VM говорит диалогу появиться вместо того, чтобы поставитьisDialogVisible = true
?IngweLand
Если честно, то у меня сложилось мнение, что у вас какое-то особое представление о MVVM. Возможно, что в нативном Android, он действительно имплементрован как-то по-особенному (а вы ссылаетесь именно на Android). Я вам советую почитать о нем на сайте Microsoft.
Да потому что как вам выше уже писали, диалоги управляются менеджером диалогов. Просто вам такой подход не понравился, и вы искренне считаете, что созданием и отресовкой диалога должна заниматся именно та страница, на которой он вызван.
Не совсем точно такая же. На мобильных устройствах диалоги, чаще всего (хотя теоретически не обязательно), перекрывают весь экран и блокируют страницу. При этом, вы (как правило) видите только один диалог. Бывают ситуации, когда диалоговое окно надо показать на уровне вашего приложения (может прилетела ошибка из другой части программы). Но показывать два диалога одновременно - будет странно. Значит вам все равно надо что-то, что управляет ими всеми. И тогда вы тащите новые зависимости в ваше вью. Можно это делать? Технически - конечно. Но тогда ваше вью все разростается и разростается.
Но диалоги - это лишь частный пример. Вам просто не нравится идея, что если какая-то логика принадлежит UI, то она может быть где-то еще. Лично я не вижу в этом никаких проблем, так как есть четкая структура. Нет никакой сложности в понимании, где находить в вашем коде именно структуру UI, а где его логика. Добавляет ли это код - да... несколько строк в отдельном файле, и все.
Между прочим, у вас вообще нигде не сказано за binding. А между прочим - это одна из ключевых характеристик паттерна. Еще раз советую побольше о нем почитать.
Если вы искренне уверены, что предложенная вами архитектура более рациональна, возможно лучше представлять ее публике в чистом виде, а не под соусом, что MVVM - антипаттерн. Я бы также подумал над лучшими примерами. Предложенные в статье лично меня не переубеждают.
Danik-ik
Простите, если глупую вещь спрошу, я с бэкэнда, а здесь в целях общего развития. Но если логика у нас в вм, то какого для кнопка должна делать чтото помимо «передать в вм событие»? В моём наивном представлении именно вм будет знать, как на это среагировать — толи сделать видимым какую-то свою часть (изменение, которое оттранслируется во вью), то ли сразу дёргать бизнес-логику. То есть вью отдаёт событие «нажата кнопка» и мы при участии уже вм либо попадаем в другое вью со своей вм, либо вм изменяет себя и вью получает событие «вот это стало видимым» если диалог почему-то, что мне странно, но допустим, интегрирован в том же вью.
То есть если мы делимся на вью и вм, то во вью логики нет. Если во вью логика есть, то мы делимся неправильно
и/или над нами тяготеет наследие Дельфиookami_kb Автор
Именно, вот я и предлагаю не делиться. И дело не в наследии Дельфи, а в том, что во Flutter'е никаких преимуществ это не дает. На мой взгляд. Об этом и статья.
IngweLand
Все правильно - это изменение вьюмодели без изменения вью. Ваше вью - это страница с кнопкой Выйти. Она останеться такой же, как и до этого. В приведенном выше условном примере используется менеджер диалогов. Он не будет частью непосредсвенно вашей страницы. Это отдельная сущность, которая отвечает за разного рода диалоги.
ookami_kb Автор
Ну там товарищ выше говорил, что при этом ViewModel не изменяется. Но не суть. Если мы используем некий менеджер диалогов, а вью про это не в курсе, то для меня это как-то странно.
Для меня (и вроде как для Flutter'а) диалоги – это точно такая же полноправная часть UI, как кнопки, надписи и прочее. Если мы выделяем менеджер диалогов, который занимается вызовом диалогов, то мы превращаем VM в Presenter. Возможно, в WPF так принято, но во Flutter я бы это не тащил.
IngweLand
Диалог - это часть UI, полноправная. Но мы же тут не говорим об изменении любого UI в вашем приложении. Мы говорим об изменении конкретной страницы. А в этом случае она останется такой же.
А зачем вашей вью (именно экрану, где располагается кнопка выхода), знать о наличии этого диалога (это и есть, кстати, пример tight coupling)? В данном конкретном случае, нажатие на кнопку Выйти просто запускает процесс выхода. А как вы структурируете этот процесс - это другое дело. Представьте, что этот процесс состоит из нескольких стадий, или с разными опциями, возможно он требует дополнительной бизнес-логики, и все эти стадии и опции никак не влияют на тот экран с кнопкой выйти.
ookami_kb Автор
Затем, что это UI-логика этого экрана. И я считаю, что он должна быть во View (а точнее, в виджете).
IngweLand
Вот что нам говорит оффсайт.
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.
Весьма близко к функциям вьюмодели.
tremp
А мне flutter_bloc больше "загтовку" для MVI напоминает...