При выполнении любого ИТ-проекта мы иногда не задумываемся о важности каждого принимаемого решения. Слишком много или слишком мало абстрагирования, использование библиотеки X, Y или Z, склеивание кода, который «будет исправлен позже», и многие другие бесчисленные примеры. У каждого из этих решений есть своя цена. Примечательно то, что эта цена укусит нас не сегодня, не завтра, а в конце концов, и когда наступит этот «возможный» день, мы можем потерять часы, дни или недели, пытаясь повторить наши шаги или исправляя беспорядок. кодовая база.

В этой статье мы расскажем об одной из таких историй. Это не статья о том, как создать «идеальную архитектуру» или лучшие подходы к каждой проблеме, это статья о том, как я боролся с этими проблемами и как я нашел решение, которое на данный момент отлично работает в моем кейс.

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

Итак, начнем наш рассказ.

Начальные блоки приложения

При переходе к Flutter и Dart может потребоваться некоторое время, чтобы понять, как здесь работает асинхронное программирование. Поскольку в Dart нет многопоточности (только концепция Isolates, о которой вы можете подробнее прочитать в этой статье от Didier Boelens: Futures - Isolates - Event Loop), мы можем полагаться только на Futures, Streams и Sink s . Но если мы хотим использовать Vanilla BLoC в нашем приложении, нам придется приложить немало усилий для их изучения.

К счастью, сама концепция BLoC обманчиво проста: BLoC - это вспомогательный класс, в котором входы - это Sink, а выходы - Stream. Мы можем использовать RxDart (если нас устраивает концепция RX), чтобы сделать нас из PublishSubjects и BehaviorSubjects, чтобы раскрыть Stream и Sink, что приведет нас к созданию BLoC, таких как следующий пример:

Цель этого простого BLoC - получить входные данные от пользователя через inputSink, добавить 1 к этому значению и затем вывести данные в outputStream. В этом примере и в остальной части статьи мы не будем комментировать реализацию этой бизнес-логики или подходы к ней.

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

Фактически, копирование и вставка нашего кода позволяет повторно использовать одни и те же функции в разных классах. Однако давайте представим, что у нас есть более 20 BLoC с вставленным кодом. В этой ситуации цена желания изменить эту функциональность будет огромной, поскольку нам придется анализировать каждый класс, изменять код и гарантировать, что он ничего не сломает на каждом экране.

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

Один из способов подойти к этому - создать базовый класс, содержащий все основные функции, но он может не подходить для каждого экрана, потому что есть определенный фрагмент кода, который существует только в 30% ваших классов. Для этого у нас есть миксины, о которых вы можете прочитать в этой статье.

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

Смешивание BLoC - там, где логика начинает рушиться

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

Мы можем разделить это приложение на 4 экрана:

  • Экран, на котором перечислены типы ингредиентов: зелень и мясо - Экран категорий
  • Экран со списком продуктов для категории: например: зелень должна иметь список с салатом, фасолью и помидорами - Экран списка товаров
  • Экран для добавления новой продукции - Экран добавления товара
  • Экран для редактирования продукта - Экран редактирования элемента

В этом случае мы можем получить следующий поток:

  • Экран категорий извлекает данные из API и передает отфильтрованный список, содержащий только правильную категорию, на Экран списка элементов .
  • Когда пользователь касается элемента, элемент передается в качестве аргумента на экран редактирования.
  • Когда пользователь нажимает кнопку «Создать новый элемент», идентификатор категории передается в качестве аргумента на экран добавления.

Как видно из схем:

  • В Category Screen мы взаимодействуем с API и передаем отфильтрованный список в List Screen.
  • Затем на List Screen экране мы передаем либо идентификатор категории, либо производственный объект в Add Screen или Edit Screen соответственно.

Здесь явно есть проблема: когда мы добавляем новые данные или редактируем данные, как мы можем снова вызвать API, чтобы получить новые данные и отобразить обновленные данные на экране List Screen?

Одно решение может полагаться на обмен данными между BLoC. С другой стороны, это решит проблему, поскольку Edit BLoC или Add BLoC могут отправлять событие прямо на Category BLoC. Но правильное ли это решение?

Поразмыслив некоторое время, мы можем прийти к нескольким причинам, чтобы не использовать этот подход:

Альтернативы? Их много, но поскольку эта статья основана на реальных событиях, мы обсудим решение, которое в то время было выбрано для этой проблемы.

Делиться единым источником истины

Одна вещь, которая до сих пор не обсуждалась, - это то, как мы обрабатываем данные.

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

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

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

Однако, как мы можем видеть из принципа, у нас есть новая проблема: вместо того, чтобы BLoC взаимодействовали друг с другом, у нас есть пользовательский интерфейс, взаимодействующий с несколькими BLoC одновременно, а с другой стороны, у нас есть несколько экранов, которые слушают одно и то же. stream, а это означает, что мы должны быть осторожны при удалении и повторном прослушивании потоков.

В нашем новом подходе могут быть и другие очевидные недостатки и положительные моменты, но прежде чем мы проанализируем его сами, мы собираемся использовать одну из основных сильных сторон Flutter: сообщество.

Архитектура обратной связи

Сообщество Flutter потрясающее, от серверов Discord и Slack до Twitter, где мы можем быстро запросить мнение или обратную связь по конкретной теме или, если мы чувствуем себя смелыми, поделиться своим кодом и запросить честное мнение.

Или, если нам повезет, мы можем спросить честного мнения у хорошего друга. Такой хороший друг, как Антонелло Галипо.

Выслушав нашу проблему и наши последние планы по проекту, он дает другую точку зрения, которая меняет наш подход к ней:

Моя идея для блока заключается в том, что он содержит всю бизнес-логику для функции (не экрана), которая может быть распределена между разными экранами. (…) У вас есть функция «управления типами». Вы их получаете, видите, редактируете, сохраняете.

Как и раньше, давайте обсудим преимущества и недостатки этого подхода.

Возможные минусы:

  • Мы отказываемся от использования BLoC для каждого экрана, и теперь у нас есть BLoC, который используется на каждом экране функции. Это означает, что, как и раньше, если у нас есть поток, который прослушивается несколькими экранами, мы должны быть осторожны в том, как мы избавляемся от BLoC и повторно прослушиваем поток, возвращаясь к экрану.
  • Мы должны обратить внимание на то, когда и как мы создаем новый экземпляр этого BLoC, поскольку, если новый BLoC будет сгенерирован, когда мы используем эту функцию, все данные будут потеряны.

Что касается плюсов этого подхода:

  • Использование одного BLoC для каждой функции означает, что у нас есть только один источник правды, поэтому на всех экранах представлены одни и те же данные.
  • Управлять одним BLoC легче, чем несколькими BLoC, поскольку нам просто нужно либо предоставить его, либо передать в качестве аргумента на следующие экраны при навигации. Это также означает, что вместо того, чтобы избавляться от каждого BLoC на каждом экране, теперь мы просто удаляем наш BLoC, если уходим от этой функции, оставляя ответственность только за одним экраном.
  • Поскольку все экраны имеют доступ к этому BLoC, при обновлении или добавлении элементов мы можем напрямую вызвать метод fetch для обновления нашего текущего списка.
  • Использование одного BLoC означает, что нам не нужно использовать initState для добавления данных в Sink каждого BLoC. Если нам не нужно использовать методы initState или dispose, это также означает, что мы можем преобразовать наши виджеты в StatelessWidgets.

Заключение

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

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

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

Наконец, я надеюсь, что эта статья послужит предостережением. Мы постоянно принимаем решения при кодировании, и иногда у нас возникает такое чувство: «Я не думаю, что это лучший способ, но я быстро его изменю в ближайшем будущем». Но затем наступает релиз. Затем появляется новая функция. Затем идет отчет об исправлении ошибки. А потом прошло 4 месяца, и мы не знаем, почему мы использовали эту конкретную логику, иногда даже с хорошей документацией. И то, что вначале было бы от 30 минут до одного часа, превратилось в бесчисленные часы исправления ошибок, поскольку у нас так много кода, который зависит от этого «быстрого и грязного исправления». Давай не будем этого делать. Если мы собираемся это исправить, давайте исправим с первого раза.