Pull to refresh

Comments 45

А был ли опыт с кроссплатформой в рамках: React Native, Ionic, Xamarin, Apache Cordova? Либо чего-то подобного.

Да, в свое время перепробовал много кроссплатформенных фреймворков. Из тех, что помню: Phonegap, Titanium, React Native, Xamarin.

Сами перешли на Flutter чтобы не мучатся два раза с Android и iOS. Но есть проблемы: иногда отсутствует SDK для нужных нам вещей. Приходится изобретать велосипед :(

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

Спасибо, отличная статья.
Интересно еще узнать как вы строите и обрабатываете формы, используя паттерн BLoC. Какие-то вспомогательные библиотеки у вас есть для этого?

Спасибо! Для форм используем стандартный флаттеровский Form+ FormField. В BLoC данные отправляются всем скопом, например, после нажатия на кнопку "Submit". Единственный момент – валидаторы хранятся в слое бизнес-логики, они используются как напрямую в виджетах, так и в BLoC'ах.

Получается в истории «я хочу чтобы кнопка submit активировалась при правильно введённых данных» — вы bool canSubmit держите не в стейте, а во view?

ps я как раз такого избегаю

Я как раз избегаю паттерна «я хочу чтобы кнопка submit активировалась при правильно введённых данных». По мне, это антипаттерн UX – кнопка должна нажиматься, триггерить валидацию и давать обратную связь пользователю.

Спасибо за статью, руковожусь таким же принципами, только избегаю codegen для стейтов/ивентов.

Есть комент к…

Следующий момент (я о нем уже упоминал, но он достоин того, чтобы по нему еще раз пройтись) – 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
С null всё понял, интутивно без манифестов в командах избегаем null и не так больно видать мне )

Про кодген — чаще в командах начинающие ребята и это накладывает ограничения.

Я использую свой подход к ивентам — делегировал им исполнение, получилось скрещение паттернов Состояние и Команда(ивенты у нас инстансы команд по сути)

каждый ивент имплементит метод,
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) хоть откуда в блоке.

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

Хмм, я пробовал такой подход. Его советуют создатели flutter_bloc. Но очень уж громоздко получается ловить изменения состояний. Не поделитесь примером кода, который показывает activity indicator и сообщение об ошибке?

По-моему, как раз ловить такие состояния получается довольно компактно и читабельно. Как-то так:


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, как-то не подумав, а теперь поняли что зря, для чего-то более важного, чем кэш, ее использовать небезопасно.

Пока нет, сейчас как раз планируем внедрять, и я, в общем-то, смотрел в сторону hive. А чем она принципиально хуже того же sqlite с точки зрения безопасности?

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

И аналога sqlcipher я не видел.

Спасибо! Полезная информация, буду думать.


Хотя нам, по сути, и нужен-то просто персистентный кэш.

Для кэша берите hydrated_bloc, если что нибудь сложнее то лучше брать moor
А мы уже год на Hive (я автор той статьи) и очень довольны.
Для случаев небольшого кеша — идеально.
Плюс удобная кодогенерация, которая хорошо скрещивается с json_serializable.
Экономит огромное количество времени.
Хотя, конечно, для сложных баз лучше использовать другие варианты. Но sqflite как то совсем не зашел.
А как у вас дела обстоят с тестами? Потому что сейчас для интеграционных тестов в официальной документации предлагается использовать библиотеку flutter_driver. При этом в issue на github'е в 2019-ом году написано о том, что команда flutter'а планирует отказываться от этой библиотеки. И в некоторых других issues по flutter_driver'у людям отвечают, что нет особого смысла ждать исправлений, ссылаясь на этот комментарий пользователя Hixie.

Интеграционные тесты – это боль. flutter_driver – очень примитивная штука, но это еще полбеды. Они регулярно падают или отваливаются по таймауту на азуровских CI-машинах с эмуляторами – хотя тут я не уверен, кого именно винить, потому что на локальной машине это воспроизвести не получается практически никогда. Потом мы эти тесты перенесли на integration_test – это вроде как раз смесь flutter_test, flutter_driver, и нативных instrumentation tests. Стало чуть получше, но все равно падают. Сейчас я планирую попробовать эти тесты запускать на реальных устройствах – либо через Firebase Test Lab, либо через App Center.

Спасибо, очень интересная статья.
А как вы работаете с BLoC и контекстом когда нужно вызвать какие-либо всплывающие элементы, например snackbar?

Спасибо!


А как вы работаете с BLoC и контекстом когда нужно вызвать какие-либо всплывающие элементы, например snackbar?

Как-то так. Там у нас на ошибку показывается диалог, но это может быть и snackbar – это в любом случае UI-логика, поэтому к блоку она не имеет отношения.

А, понял, все таки приходится туда-сюда context передавать. Мне как-то очень криво показалось все время так делать.
Из-за этого в наших проектах мы перешли с BLoC на Getx — до сих пор глядя на код блоков радуюсь переходу, код на Getx получается элегантнее.
Это личное мнение, просто хотел поделиться, вдруг зайдет вам тоже.

Нет, context туда-сюда передавать точно не надо. Код, который я приводил в ветке – это код виджета. Блок знать не знает ни про какой контекст.


Getx смотрел, но идея "фреймворка для фреймворка" мне совершенно не зашла.

А вот тут, displayError у вас где живет? Вы в него передаете context чтобы отобразить всплывающее сообщение, правильно?
Интересно как вы отделяете код сообшений, я в свое время не смог его разумно выделить в контроллеры без контекста.
То есть реального 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 просто отобразит этот текст (сам текст приходит с бэкенда).
Спасибо. Я именно showDialog имею в виду. Например, когда приходит пуш уведомление, а приложение открыто, то мы можем быть на любом экране приложения.

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

Вот у нас висит 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), если интересно, то меня бы это мотивировало для статьи.
Можно написать два приложения на 2 примерах (я на getx), если интересно, то меня бы это мотивировало для статьи.

Да, идея мне нравится.

Спасибо, очень ценная статья, видео тоже было топовое по содержанию когда я его встретил.

Было бы безумно интересно почитать новую редакцию этой статьи в стиле "что изменилось за год с момента выпуска видео".

Спасибо, рад, что понравилось.

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

Sign up to leave a comment.

Articles