Comments 45
А был ли опыт с кроссплатформой в рамках: React Native, Ionic, Xamarin, Apache Cordova? Либо чего-то подобного.
Интересно еще узнать как вы строите и обрабатываете формы, используя паттерн BLoC. Какие-то вспомогательные библиотеки у вас есть для этого?
Спасибо! Для форм используем стандартный флаттеровский Form
+ FormField
. В BLoC данные отправляются всем скопом, например, после нажатия на кнопку "Submit". Единственный момент – валидаторы хранятся в слое бизнес-логики, они используются как напрямую в виджетах, так и в BLoC'ах.
ps я как раз такого избегаю
Есть комент к…
Следующий момент (я о нем уже упоминал, но он достоин того, чтобы по нему еще раз пройтись) – NoSuchMethodError (здравствуй, Java с ее NullPointerException). Говорят, скоро все будет хорошо, осталось всего лишь дождаться миграции самого Flutter'а и всех библиотек, но пока – что есть, то есть.
noSuchMethod актуален только при вызове на dynamiс, вызвать любой метод на null невозможно и нет никакой связи с Java NPE
Собственно прошу объяснить, вдруг это у меня лыжи не едут, либо поправить статью =)
За статью спасибо!
Спасибо! А почему избегаете?
noSuchMethod актуален только при вызове на dynamiс, вызвать любой метод на null невозможно и нет никакой связи с Java NPE
Механизмы у них разные, но с практической точки зрения проблема одна и та же:
String x = null;
x.toUpperCase(); // NoSuchMethodError
String x = null;
System.out.println(x.toUpperCase()); // NullPointerException
Про кодген — чаще в командах начинающие ребята и это накладывает ограничения.
Я использую свой подход к ивентам — делегировал им исполнение, получилось скрещение паттернов Состояние и Команда(ивенты у нас инстансы команд по сути)
каждый ивент имплементит метод,
abstract class BlocEvent {
Stream<Object> mapEventToState(Object currentState, B bloc);
текущий стейт специально добавлен чтобы не пользовать геттер bloc.state и получить доступ к стейту
if (state is ...) state.foo
Получается bloc является контекстом паттерна состояния и содержит только зависимости
class BarBloc extends BaseBloc<...,...> {
final Api api;
final Foo foo;
}
А BaseBloc делегирует исполнение ивентам, по сути являясь диспатчером очереди команд
class BaseBloc extends Bloc<...,...> {
@override
Stream<BlocState> mapEventToState(BlocEvent event) async* {
yield* event.mapEventToState(state, this);
}
}
Плюсы
- быстрый переход к коду — тыц на ивенте и ты уже видишь что хотел
- более читабельнее — читаешь отдельную команду и не листаешь весь класс блока до метода _mapEvent
- переиспользование ивентов ввода(с валидацией) в разных блоках формах ввода — телефон, пароль, код из смс
В 2ух словах так)
Интересный подход, но мне приходят в головы минусы, из-за которых я бы не стал так делать:
Object currentState
– слишком общий базовый класс, придется делать даункастинг "вслепую", что-то забыл, и компилятор не подскажет.- Переиспользование ивентов означает, что ивент должен быть в курсе о разных классах блока.
- Нарушение SRP. Event – это event, его задача – передать данные о том, что что-то произошло. Что с этим делать – задача блока.
Вообще, паттерн Команда – это же по сути костыль, нужный в языках, где нет поддержки first-class functions. Если блок разрастается, и отдельный обработчик ивента становится слишком сложным, я бы его просто вынес в отдельную функцию (собственно, у нас и так каждый обработчик – это отдельный метод, но внутри блока, а при желании можно вынести и наружу).
- Object currentState можно специфицировать дженериком, мне всё не начать рефакторинг
- переиспользуемые ивенты знают только о родительском FormBloc, им не нужно знать в какой конкретный блок его отправляют, достаточно чтобы он был наследником
- про нарушение SRP категорически не согласен
Про Команду
Если у команды есть нагрузка(ex. user input), то без класса сложно обойтись и как раз нагрузку в конструктор и укладываю, а сигнатура метода выполнения остаётся неизменной.
Про несогласие о нарушении SRP…
The Single Responsibility Principle (SRP) states that each software module should have one and only one reason to change.
Ивенты блока и сам блок — это один компонент логики Business Logic Component, который вполне себе отдельный software module.
Нарушен SRP если этот модуль могут попросить изменить к примеру и Аналитик, и UX'ксер
Хабр статья про SOLID
На каждом проекте люди играют разные роли (actor): Аналитик, Проектировщик интерфейсов, Администратор баз данных. Естественно, один человек может играть сразу несколько ролей. В этом принципе речь идет о том, что изменения в модуле может запрашивать одна и только одна роль. Например, есть модуль, реализующий некую бизнес-логику, запросить изменения в этом модуле может только Аналитик, но никак не DBA или UX.
Ссылка на оригинал SRP от R. C. Martin
Посему не считаю SRP нарушенным =)
Я не пропагандирую такой подход =)
Я понимаю, мы ж просто обсуждаем плюсы и минусы разных подходов.
про нарушение SRP категорически не согласен
Я зря назвал это нарушением SRP, признаю. Я сам же в комментариях к другой статье говорю, что это неправильно. Но проблема всё равно, на мой взгляд, остается. Просто это скорее про coupling/cohesion – с одной стороны, event знает слишком много про логику обработки, с другой – эта самая логика обработки оказывается размазанной по нескольким классам (в т.ч. и в самом блоке логика будет – как иначе сделать, например, debounce ивентов?)
переиспользуемые ивенты знают только о родительском FormBloc, им не нужно знать в какой конкретный блок его отправляют, достаточно чтобы он был наследником
А если в какой-то момент поведение станет специфичным для подтипа этого блока?
Если у команды есть нагрузка(ex. user input), то без класса сложно обойтись и как раз нагрузку в конструктор и укладываю, а сигнатура метода выполнения остаётся неизменной.
Можно взять функцию высшего порядка. Хотя, возможно, это просто вкусовщина.
А если в какой-то момент поведение станет специфичным для подтипа этого блока?
Я переиспользую только Input ивенты в FormBloc — сами ивенты только меняют свой инпут в стейте и более ничего — вряд ли что-то сильно измениться =)
Но проблема всё равно, на мой взгляд, остается. Просто это скорее про coupling/cohesion – с одной стороны, event знает слишком много про логику обработки, с другой – эта самая логика обработки оказывается размазанной по нескольким классам
Не согласен, coupling/cohesion применимы к модулям. В разрезе Event/Bloc классы ивентов и класс блока — это один модуль и они поумолчанию тесно связаны(coupling) «ивенты конкретного блока». Это сильно связанные классы для реализации одной какой-то логики/компонента.
Про зацепление(cohesion). Если бы я в Event/Bloc засылал BuildContext — это действительно было бы сильным зацеплением на Flutter framework, но такое разумеется пресекается.
(в т.ч. и в самом блоке логика будет – как иначе сделать, например, debounce ивентов?)debounce сложной/долгой операции — это не логика фичи, это ограничения для жизни в реальной среде с нагрузками на сеть/cpu. Поэтому debounce делаю в Bloc.transformEvents разделяя Event и _DebouncedEvent, последний выполняет долгую/сложную/нагрузо_нежалательную операцию
Я переиспользую только Input ивенты в FormBloc — сами ивенты только меняют свой инпут в стейте и более ничего — вряд ли что-то сильно измениться =)
Но я так понимаю, это просто на уровне соглашения, так? Компилятор же никак не помешает переиспользовать любые ивенты в любых блоках.
Не согласен, coupling/cohesion применимы к модулям.
Не обязательно, это вполне применимо и к классам. В вашем варианте блок знает про ивенты и стейт, ивенты знают про блок и стейт. В моем варианте блок знает про ивенты и стейт, но ивенты ничего не знают ни про блок, ни про стейт.
Но я так понимаю, это просто на уровне соглашения, так? Компилятор же никак не помешает переиспользовать любые ивенты в любых блоках.
Статический анализатор покажет что событие не в тот блок добавляем, здесь всё как с обычным блоком
Не обязательно, это вполне применимо и к классам. В вашем варианте блок знает про ивенты и стейт, ивенты знают про блок и стейт. В моем варианте блок знает про ивенты и стейт, но ивенты ничего не знают ни про блок, ни про стейт.
тут дело вкуса и сложно вынести из беседы в коментах какой либо артефакт =) предлагаю остановиться =)
По мне, в этом мало смысла, я предпочитаю состояние и логику UI держать в слое UI. BLoC должен отвечать за логику более высокого уровня (логику приложения, или даже бизнес-логику, как подсказывает название).
Как вы отображаете activity indicator в этом случае? Как скрываете его после выполнения операции и как отображаете ошибку?
Советую посмотреть в сторону cubit. Тот же блок, только без евентов. Состояния можно емитить (emit) хоть откуда в блоке.
Как вы отображаете activity indicator в этом случае? Как скрываете его после выполнения операции и как отображаете ошибку?
У блока может быть состояние Loading
, когда он чем-то занят, и состояние Error
, если произошла ошибка. Это не логика UI, это логика данных. Логика UI – показывать ли при этом уведомление, или модальный диалог, или вообще игнорировать.
Советую посмотреть в сторону cubit. Тот же блок, только без евентов. Состояния можно емитить (emit) хоть откуда в блоке.
Смотрел, для простых случаев – удобно. Для более сложной логики "классический" блок дает более прямолинейный и поддерживаемый код (хоть и ценой большего бойлерплейта).
По-моему, как раз ловить такие состояния получается довольно компактно и читабельно. Как-то так:
return BlocConsumer<MessagesBloc, MessagesState>(
listener: (context, state) {
state.threadsUpdatingState.maybeWhen(error: (e) => displayError(context, e), orElse: ignore);
},
builder: (context, state) =>
Scaffold(
appBar: CommanderAppBar(
title: widget.appBarTitle,
actions: <Widget>[
RefreshButton(
isRefreshing: state.threadsUpdatingState is ProcessingStateLoading,
onPressed: () => context.read<MessagesBloc>().add(const MessagesEvent.fetchThreadsRequested()),
),
],
),
body: Container(), // Everything else
),
);
Эта библиотека используется в основном в слое API. Она экономит кучу времени, нервов и кода, поскольку умеет генерировать код для сериализации/десериализации DTO и преобразования в/из Dart-классов.
Почему бы не использовать freezed для этих классов? Он умеет в сериализацию «из коробки» и заодно получаете иммутабельные классы.
Один из самых важных для меня пунктов – обработка ошибок. Если у вас в Dart'овском коде выбросится исключение, приложение не упадет.
Если не ошибаюсь, не упадет при дебаг-сборке. Но упадет в релизе.
Почему бы не использовать freezed для этих классов? Он умеет в сериализацию «из коробки» и заодно получаете иммутабельные классы.
freezed просто делегирует сериализацию этой же самой библиотеке. А поскольку для DTO copyWith
не нужен, нет смысла в дополнительной прослойке. Ну а классы и так иммутабельные – все свойства final
.
Если не ошибаюсь, не упадет при дебаг-сборке. Но упадет в релизе.
Не упадет ни там, ни там.
Пока нет, сейчас как раз планируем внедрять, и я, в общем-то, смотрел в сторону hive
. А чем она принципиально хуже того же sqlite с точки зрения безопасности?
И аналога sqlcipher я не видел.
Спасибо! Полезная информация, буду думать.
Хотя нам, по сути, и нужен-то просто персистентный кэш.
Для случаев небольшого кеша — идеально.
Плюс удобная кодогенерация, которая хорошо скрещивается с json_serializable.
Экономит огромное количество времени.
Хотя, конечно, для сложных баз лучше использовать другие варианты. Но sqflite как то совсем не зашел.
Интеграционные тесты – это боль. flutter_driver
– очень примитивная штука, но это еще полбеды. Они регулярно падают или отваливаются по таймауту на азуровских CI-машинах с эмуляторами – хотя тут я не уверен, кого именно винить, потому что на локальной машине это воспроизвести не получается практически никогда. Потом мы эти тесты перенесли на integration_test
– это вроде как раз смесь flutter_test
, flutter_driver
, и нативных instrumentation tests. Стало чуть получше, но все равно падают. Сейчас я планирую попробовать эти тесты запускать на реальных устройствах – либо через Firebase Test Lab, либо через App Center.
А как вы работаете с BLoC и контекстом когда нужно вызвать какие-либо всплывающие элементы, например snackbar?
Спасибо!
А как вы работаете с BLoC и контекстом когда нужно вызвать какие-либо всплывающие элементы, например snackbar?
Как-то так. Там у нас на ошибку показывается диалог, но это может быть и snackbar – это в любом случае UI-логика, поэтому к блоку она не имеет отношения.
Из-за этого в наших проектах мы перешли с BLoC на Getx — до сих пор глядя на код блоков радуюсь переходу, код на Getx получается элегантнее.
Это личное мнение, просто хотел поделиться, вдруг зайдет вам тоже.
Нет, context туда-сюда передавать точно не надо. Код, который я приводил в ветке – это код виджета. Блок знать не знает ни про какой контекст.
Getx смотрел, но идея "фреймворка для фреймворка" мне совершенно не зашла.
Интересно как вы отделяете код сообшений, я в свое время не смог его разумно выделить в контроллеры без контекста.
То есть реального decoupling с UI у меня не получилось. Поэтому любопытно :)
return BlocConsumer<MessagesBloc, MessagesState>(
listener: (context, state) {
state.threadsUpdatingState.maybeWhen(error: (e) => displayError(context, e), orElse: ignore);
},
displayError
– это просто обертка над showDialog
, т.е. это все еще UI-слой.
Интересно как вы отделяете код сообшений, я в свое время не смог его разумно выделить в контроллеры без контекста.
Если вы имеете в виду сам текст ошибки, то у нас могут быть такие ситуации:
- стандартная ошибка, например, ошибка сети. В этом случае из блока придет какой-нибудь тип
NetworkError
. Дальше уже ответственность UI превратить его во что-нибудь типа: "Ошибка сети. Попробуйте еще раз". - ошибка валидации на стороне бэкенда (например, у пользователя недостаточно прав). Тогда блок выдаст, например,
BackendError('У вас недостаточно прав для этой операции')
, и UI просто отобразит этот текст (сам текст приходит с бэкенда).
По логике пушей, при открытом приложении они не отображаются в панели телефона, их должно отобразить приложение.
Вот у нас висит listener, он получил пуш уведомление.
В случае когда мы используем Get, мы просто без контекста выводим сообщение, прямо из контроллера:
Get.snackbar('title', 'body');
В случае с BLoC не получалось так просто и понятно делать. Приходилось передавать контекст и как-то сильно мудрить. Уже точно не помню все нюансы.
Так показ пуша – это тоже логика UI. Пришел пуш, приложение открыто, UI отображает сообщение – блок вообще за это не отвечает.
А вот это:
В случае когда мы используем Get, мы просто без контекста выводим сообщение, прямо из контроллера
мне как раз не нравится. Мы из блока дергаем UI, только это еще и происходит неявно. В лучшем случае – мы превращаем блок в презентер, в худшем – получаем неявную зависимость на UI слой.
Я не соглашусь, но наверное я непонятно пишу вопрос.
Моя точка зрения в том, что BLoC, Redux, Mobx, Getx — это инструменты (подходы, паттерны) для управления состоянием приложения.
Например в случае с пушем — пришел пуш, в нем есть данные, нам надо:
1. отобразить пуш с картинкой
2. потом юзер нажал
— надо получать данные, менять состояние интерфейса.
Соответственно это всё живет в бизнес логике. А как это разруливать — есть типовые варианты.
Мы в проектах пробовали BLoC и Getx. Не скрещивали, а в разных проектах разные паттерны.
Сугубо личное мнение — Getx позволяет убрать кучу бойлеплейта, делает код более понятным и читаемым.
И еще раз спасибо — не хватает таких статей, где суммирован реальный опыт.
Спасибо!
Единственного правильного варианта, конечно, быть не может, иначе всё было бы слишком просто. Я думаю, я понял вашу точку зрения. Как мне кажется, ключевое отличие здесь: "потом юзер нажал — надо получать данные, менять состояние интерфейса. Соответственно это всё живет в бизнес логике." С моей точки зрения, состояние интерфейса не должно жить в бизнес логике и, соответственно, в BLoC'е.
На примере тех же пуш-уведомлений: у нас есть пуши на новые сообщения. Если приложение в фоне, мы показываем стандартное уведомление. Если развернуто, показываем уведомление внутри приложения. Но если пользователь уже находится в текущей беседе, то мы не показываем ничего, просто обновляется ветка. И если за уведомление отвечает блок, то получается, что он еще должен быть в курсе, на каком экране мы находимся – а этого я однозначно хотел бы избежать.
Вообще, эта тема заслуживает отдельной статьи (а то и не одной). BLoC, на мой взгляд, это не просто "реактивная ViewModel", когда View делается максимально "тупым". С блоком у View может и должна быть своя логика, и это, как мне кажется, противоречит большой (если не большей) части взглядов на архитектуру мобильных приложений. Но пока что мне нравится, в каком направлении развивается наше приложение, поэтому я попробую более подробно описать эту идею, с обсуждением преимуществ и недостатков.
Будем ждать новых статей. Было бы круто приложить простейшее приложение-пример, убирает кучу вопросов сразу и предмет обсуждения появляется.
Я тоже все не могу выделить время, написать как раз про push'ы — пример как показать уведомление с данными, перекинуть куда надо и т.п.
Можно написать два приложения на 2 примерах (я на getx), если интересно, то меня бы это мотивировало для статьи.
Спасибо, очень ценная статья, видео тоже было топовое по содержанию когда я его встретил.
Было бы безумно интересно почитать новую редакцию этой статьи в стиле "что изменилось за год с момента выпуска видео".
1 год с Flutter в продакшне