Как стать автором
Обновить

Комментарии 21

Хорошо бы кто рассказал про стейт менеджеры, а то их так много развелось… под флаттер…

В нашей статье мы не разрабатывали новый StateManager, а использовали один из наиболее популярных - Bloc, с небольшой доработкой SingleResult для приближения к MVI. Но ваша идея хорошая, постараемся в будущем разработать статью, сравнивающую разные StateManagment решения, возможно в окружении нашего “шаблона“.

просто по мне стейт менеджеры и их наличие во флаттер комьюнити вводит немного в замешательство, bloc ведь тоже эволюционировал, завезли кубиты и не просто так, а потому что решения от комьюнити показали эволюционное удобство. Сколько вокруг GetX возни, правильно его использовать если он кода просит писать куда меньше, не правильно… riverpod, MobX…

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

я остановился на связке MobX & Provider, и очень ей доволен, bloc кажется неоправданно перегруженным, хотя может там чтото поменялось за последнее время.

НЛО прилетело и опубликовало эту надпись здесь

Ага, и лично у меня после ознакомления с этим зоопарком возникло одно простое чувство: "а гори они все!.. В принципе, и setState сам по себе довольно хорош."

огого, вот это статьища! здорово развернул ту коротенькую новость с прошлой пятницы)

НЛО прилетело и опубликовало эту надпись здесь

Благодарим за обратную связь, мы всегда рады услышать мнение других и понять движемся ли мы в правильную сторону! 

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

1. Const - подразумевают исключительно константы. Если называть их util, подразумеваются утилитарные вещи - константы / общие функции без домена / экстеншены над dart core. У нас был такой опыт, утилиты стали доходить до нескольких тысяч строк кода, поэтому мы приняли решение сделать более жесткое сужение.

2. Про arch вы действительно правы, мы указали, что в идеальном мире они должны быть вынесены в отдельные pub-package, но на этапе, пока решение не задокументировано и покрыто тестами, его лучше не выливать в package для других внутренних/внешних потребителей. Сделать arch саб-модулем решение хорошее, мы к нему тоже пришли. Про свой опыт модуляризации, в том числе пакета arch, планируем рассказать в будущем, это достаточно неоднозначная тема.

3. mappers - вещь опциональная, даже на уровне статьи мы нигде не указываем ее, как обязательную часть нашей структуры директорий. Хотим заметить, что их область использования чуть шире, чем fromJson и toJson, потому что вы можете работать не только с REST, а также в Mapper вы можете добавить проверки соблюдения контракта с бекендом, аналитику нарушений контрактов и тд. Из-за этого логика маппинга начинает распухать и раздувать модели, что, по-нашему мнению, удобнее вынести в изолированный объект. Вполне возможно наше восприятие было искажено длительной Kotlin/Java разработкой, и поэтому мы приходим к таким выводам.

4. Вы правы, SingleResult фактически и есть StreamController, развёрнутый над Bloc, и StreamListener, развёрнутый в SrBlocBuilder, поэтому не очень понятно, в чем состоит костыльность абстракции. Ввод дополнительной абстракции, полностью совместимой с оригинальным Bloc, мы обосновываем экономией кода в каждом отдельном Bloc, но самое важное - мы можем строить инфраструктуру вокруг этой абстракции: централизованное логирование SR, аналитика, реализация TimeMachineDebuger с учетом SR. 

5. Мы пробовали строить приложение исключительно на Provider, но скорость разработки значительно уменьшалась при росте используемых объектов. Когда проект разрастается и подходит к 30, 50 тысячам строк кода, собирать объекты руками становится достаточно проблематично. А для понимания ЖЦ объекта приходится обращаться к дереву виджетов, что не всегда очевидно, плюс иногда было необходимо обращаться к дереву без контекста.  

Мы не исключаем, что мы просто не смогли его приготовить, но решение GetIt + Injectable позволило решить все наши боли без видимых потерь и повысить скорость работы. Мы не сделали абсолютно все объекты на GetIt, а использовали Provider(BlocProvider) из-за не стандартного ЖЦ объекта (factory/singleton), BLoC должен вести себя как Scoped-объект. А наши тесты использования Scope в GetIt показали, что проект начинает усложняться и пока она нужна только для StateManagment-объектов, нам удобнее использовать BlocProvider. 

6. Мы упомянули в статье, что ThemeData + ColorScheme + TextTheme не бился с дизайн-токенами, которые были сформированы в нашей компании задолго до начала разработки на Flutter и поэтому поделились свои решением, которое позволяет решить эту проблему. Нам показалось, что это достаточно полезно для команд в похожей ситуации. Также решение в виде централизованного Bloc над сменой темы позволяет включать всю инфраструктуру Bloc (логирование, аналитика и так далее) в этот процесс, а также менять тему без контекста. Хотим заметить, что мы сделали минимально необходимый mapping нашего объекта темы в стандартную тему.

НЛО прилетело и опубликовало эту надпись здесь

Сейчас в проекте у нас сгенерированый файл с деревом зависимостей приложения занимает 600 строк, но это не человеко-читаемый код, потому что он генерируется, если бы мы его писали руками и с использованием Provider, он скорее всего бы дошел до 1000-1500 строк. Плюс проект у нас пока достаточно молодой, если апроксимировать цифры на наш основной нативный проект, то это была бы история про 10000+ строк.

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

Однако для нашей команды намного привычнее концепция GetIt для управления зависимостями, весь инфраструктурный код генерируется, а ЖЦ каждого объекта наглядно виден в коде самого объекта. Это досточно серьезные плюсы для нашей команды, и при этом мы не видим каких-то минусов или проблем, создаевамых из-за использования GetIt+Injectable.

Отличная статья.
Когда я пару месяцев назад переходил с Android на Flutter, начал исследование возможных комбинаций для реализации своих проектов и собрал набор решений, которые на 90% сходятся с вышепредставленными: от похожей структуры папок до теммирования приложения и набором библиотек.
Но изыскания продолжаются и люди в комментариях дают дельные советы, к которым, как минимум, я постараюсь прислушаться и вразумиться :)

статья отличная, но вот до сих пор не могу понять почему используют Either? не проще отлавливать исключения в нужном месте?

Благодарим вас за ответ!

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

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

Следовательно, мы разделяем для себя 2 вида ошибок:

– Критические ошибки, такие выбрасываются через throw и не обрабатываются в бизнес логике, потому что это какое-то критическое исключение.

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

Таким образом Either позволяет сделать наш код чище и сильно проще для восприятия, а также вы неизбежно будете обрабатывать ветку left и не забудете об обработке ошибок.

спасибо за такой развернутый ответ =). попробую на проекте использовать подобное

НЛО прилетело и опубликовало эту надпись здесь

Мы согласны, что throw является допустимым подходом для обработки ошибок и исключений, особенно с учетом разделения Error и Exception в Dart. Но давайте я попробую расшифровать, какие структурные проблемы решает Either в нашем примере использования.

В Дарте есть разбор ошибок по типам:

Это больше история про реализацию, а не про контракт. Например, у вас есть интерфейс:

abstract class AuthInteractor {
  Future<bool> authorize();
}

Пока вы не обратитесь к его реализации, вы не сможеет узнать, выбрасывает он UnAuthorizedError или нет (может он вообще ходит в сеть и выбрасывает Http-ошибки), а если реализаций несколько, то все еще хуже, ошибки могут быть разные, а ваш код об этом даже не узнает. В java, например, было ключевое слово 'throws', позволяющее указать на уровне интерфейса, какие ошибки выбрасывает метод, также оно заставляет вас обработать эти ошибки в месте вызова. В Dart, кажется, такой функциональности нет.

А если говорить об Either, то вы четко на уровне контракта описываете допустимые ошибки:

abstract class TimeApiService {
  Future<Either<CommonResponseError, TimeResponse>> getTime();
}

При вызове такой функции вы получите Either, а не результат TimeResponse, что гарантированно заставит вас обработать ошибку (CommonResponseError). В случае, если тип ошибки изменится, то типизация вам подскажет все места, где сломался контракт.

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

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

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

Это нормальная правильная обработка ошибок, вы к ней просто не привыкли.

После досточного длительного опыта работы в нативной разработке с Java/Kotlin, где сильно распространены throw/try/catch, довольно сложно к ней "не привыкнуть" :)

НЛО прилетело и опубликовало эту надпись здесь

Спасибо за классную статью!
Вопрос про базы - а зачем вы используете и SharedPreferencies и отдельную базу?
Не проще ли было бы создать в основном хранилище таблицу, например, UserSettings и там хранить настройки и заодно через API синхронизировать настройки на разных дивайсах?

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

В нашем примере Moor -«тяжелая» база со всеми возможностями но и с тяжелым и ресурсоемким движком. SharedPreferences - легковесная база (базируется на нативных легковесных решениях, с урезанным функционалом) но с легким запуском движка. Использование разных решений - это вопрос оптимизации мобилок. Бывают случаи, когда нет необходимости поднимать движок "тяжелой" базы, и сохранить какой-либо признак достаточно в легковесной базе.

И key-value и реляционные хранилища имеют свои преимущества в разных кейсах использования, которые актуальны для мобилок. Также отмечу, что использование 2 разных каналов общения с хранилищами, чуть повышает отказоустойчивость, а особенно в нашем самом проблемном кейсе - изолятах (бэкграунд сервис). Чем чаще выполняются параллельные запросы в Moor, тем больше она сбоит (что мы наблюдали в крашлитике), а SharedPreferences себя хорошо показывают в этом кейсе (за счёт нативной safe-multi-thread реализации)

Зарегистрируйтесь на Хабре, чтобы оставить комментарий