Pull to refresh

Comments 43

А был ли опыт с кроссплатформой в рамках: 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), если интересно, то меня бы это мотивировало для статьи.

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

Only those users with full accounts are able to leave comments. Log in, please.