Меня зовут Семен Плевако, Flutter разработчик в Центре развития финансовых технологий (ЦРФТ) Россельхозбанка. Сегодня поговорим про применение БЭМ методологии в проектах на Flutter.

Я, как и многие Flutter разработчики, мигрировал из веб-разработки. По инерции хотелось использовать те же подходы к вёрстке и управлению состояниями. Если во втором случае можно было взять MobX или BLoC и получить что-то очень близкое к популярным веб фреймворкам, то с вёрсткой было не все так однозначно.

Надо заметить, что в вебе не использовался БЭМ «на всю катушку». В моих проектах не было ни сборщиков, ни BEMJSON, использовалась только малая часть инструментов: подход к названию компонентов и, так называемое, "мышление по БЭМ".

Подробнее почитать о методологии можно здесь.

Что мы хотим добиться?

  • Улучшить читаемость кода за счет нейминга;

  • Избавиться от очень объемных по коду виджетов;

  • Повторного использования кода;

  • Упростить адаптивную верстку;

  • Плавного переноса проекта на БЭМ;

  • Более быстрого создания компонентов и элементов за счет сниппетов.

Нейминг

Внутри команды самое большое сопротивление встретила необходимость отхода от общепринятых правил нейминга. Но со временем мы убедились, что добавить исключение в правила линтера было не такой и плохой идеей. Теперь компоненты всегда будут в CamelCase с нижними подчеркиваниями, также как файлы и директории. (Здесь есть некоторое отхождение и от БЭМ. Элементы у нас отделяются двумя нижними подчеркиваниями, а модификаторы одним). Например, компонент «домашняя страница» будет иметь свою директорию HomePage – это и название компонента, в этой директории будут лежать следующие файлы:


HomePage
– это компонент;

HomePage__appBar – это виджет элемент, который не может существовать отдельно;

HomePage_tablet - это модификатор, виджет, который будет отображаться для планшетной версии.

Чтобы мы могли создавать файлы и классы такого вида, нам нужно отключить правила линтера в файле analysis_options.yaml

include: package:flutter_lints/flutter.yaml

linter:
  rules:
    lowercase_with_underscores: 0
    camel_case_types: 0

Плавный переход на БЭМ

Чтобы начать создавать компоненты, вам не нужно переписывать весь проект. Достаточно ввести договоренности в команде. Отличать БЭМ виджет от других очень просто: БЭМ-компонент всегда имеет название файла типа Name__Element_modificator и название директории совпадает с названием компонента так же, как и название классов.

Остальные файлы проекта (стейты, модели, репозитории, и т.д.) оставляем без изменения, согласно рекомендациям линтера. В формате discount_page_state.dart

Когда вы будете заниматься рефакторингом старых виджетов, вы можете на своё усмотрение перемещать виджеты на БЭМ. При этом все итерации не будут создавать больших проблем, и со временем весь проект станет единообразным.

Как быстро создавать компоненты и элементы

Когда начнете создавать виджеты по БЭМ вы заметите, что приходится довольно много создавать небольших виджетов. Для этого были сделаны сниппеты под Android Studio.

  1. Чтобы добавить свой сниппет, перейдите в Android Studio -> Preferences -> Live Templates; 

  2. Создайте группу под названием «custom»;

  3. Добавьте сниппет izBem;

    import 'package:flutter/material.dart';
    
    class $NAME$ extends StatelessWidget {
      const $NAME$({
        Key? key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Container($END$);
      }
    }
  4. Также выберите контекст Dart;

  5. Измените значение переменной Name, установите значение fileNameWithoutExtension() как показано на скриншоте;

Теперь всё готово, чтобы быстро создавать компоненты или элементы.

Для VSCode создание сниппетов практически ничем не отличается, однако синтаксис будет немного другой.

Code -> Preferences -> UserSnippets;

"BEM easy component": {
		"scope": "dart",
		"prefix": "izBem",
		"body": [
			"import 'package:flutter/material.dart';",
			"",
			"/// Компонент ${1:}",
			"class $TM_FILENAME_BASE extends StatelessWidget {",
			"\tconst $TM_FILENAME_BASE({Key? key}) : super(key: key);",
			"",
			"\t@override",
			"\tWidget build(BuildContext context) {",
			"\t\treturn Container();",
			"\t}",
			"}"
		]
	},

Порядок создания компонента такой

  1. Придумываете название этого компонента, например LoginPage;

  2. Создаете в папке LoginPage файл LoginPage.dart;

  3. После создания в файле сразу вводите izBem и нажимаете Enter;

  4. Сниппет возьмет название файла и создаст виджет с таким же названием;

  5. Чтобы создать элемент, скопируйте название компонента и создайте рядом новый файл с двумя подчеркиваниями и названием элемента LoginPage__element, либо модификатора LoginPage__element_tablet и также используйте izBem.

У нас есть и другие сниппеты, например позволяющие быстро создать шаблон стейта для МobX. Вообще сниппеты крайне удобный инструмент, который можно гибко настроить под нужды вашего проекта. Что же касается БЭМ подхода, то вы можете самостоятельно дописать в сниппет свои конструкции, которые будут нужны исключительно в вашем случае. Главное здесь придерживаться правил нейминга.

Адаптивная вёрстка

Чтобы быстро и эффективно разрабатывать виджеты для различных размеров экранов, можно использовать утилиту responsive которую можно также найти в этом репозитории в lib/utils/responsive.dart скопируйте его себе в проект

С помощью данной утилиты можно писать вот такой код:

@override
Widget build(BuildContext context) {  
  return responsive(
    context,
    phone: const LoginPage_phone(),
    tablet: const LoginPage_tablet(),
  );
}

При изменении размеров экрана будет изменяться дерево виджетов в зависимости от модификаторов.



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

Также как прописывать модификаторы для компонента можно прописывать модификаторы для элементов: LoginPage__hello_phone LoginPage__hello_tablet

 Размеры экранов зафиксированы, вы можете изменить их сами, если посчитаете нужным:

const double ResponsiveMinPhoneSize = 480.0;
const double ResponsivePhoneSize = 640.0;
const double ResponsiveTabletSize = 768.0;

Также в этой утилите есть дополнительные функции, которые могут вам пригодиться.

Избавляемся от антипаттерна

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

Так как форма логина может существовать отдельно от страницы логина, то мы её вынесем в отдельный компонент. Для примера я вынес в отдельный элемент label, и посмотрите, насколько легче воспринимать такую структуру. Если вы ранее занимались вёрсткой под веб-приложения по БЭМ – вы должны оценить.

Мы сразу улучшаем читаемость и уменьшаем основной файл в 2 раза, также виджет LoginForm__field стал константой, что положительно влияет на производительность. Так как константы лишний раз не перерисовываются в отличие от функции.

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

  • Читаемость кода сильно увеличилась, что положительно сказывается на погружении новых разработчиков в проект;

  • Разбиваем виджеты на более мелкие вместо использования build-методов. При использовании MobX оборачивание самого конечного легковесного виджета даёт приличную оптимизацию, поскольку рендериться при изменении будет только он;

  • Также есть в планах десктопные версии и адаптивная вёрстка (сейчас опробовали на уровне PoC), а БЭМ методология отлично, если не идеально подходит для такого рода задач.

Код можно посмотреть здесь.

На этом пока все! Спасибо что дочитали. 

В следующей статье расскажу, как использовать данный подход в связке с MobX и Provider, как создавать отзывчивые компоненты без единого (почти) StatefulWidget. Подпишись, чтобы не пропустить.

Комментарии (18)


  1. DVF
    08.06.2022 17:04
    +4

    Вот зачем всё это переносить из веба в разработку приложений?


    1. Semapl3 Автор
      08.06.2022 17:24
      +1

      Справедливый и популярный вопрос)

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

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


  1. Valeradomostroy
    08.06.2022 17:17

    Почему не используешь build-методы?


    1. Semapl3 Автор
      08.06.2022 17:27
      +2

      build методы - это самый легкий путь превратить код в портянки по 1000+ строк
      лучше выносить в отдельные виджеты делать их константами это удобней читать + для таких виджетов не будет вызываться ребилд при ребилде основного виджета


  1. tbl
    08.06.2022 18:43

    Судя по тому, как создатель BEM (Yandex) забил на него (последние существенные коммиты в github.com/bem были сделаны более года назад, а так bem-tools вообще заморожен), подозреваю, что это уже заброшенная методология, которая скорее всего не будет развиваться, и от которой отказываются. Странно, зачем тащить мертвеца куда-то в новое место?


    1. Semapl3 Автор
      08.06.2022 19:16

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

      В Яндексе в вакансиях до сих пор пишут:

      Наверное hr копируют по привычке. Надо им подсказать что БЭМ уже умер)

      Скорее реакт умрет чем БЭМ)


      1. fougasse
        08.06.2022 20:55

        Вне рунета оно кому-то нужно?


        1. DVF
          08.06.2022 21:25

          Местами в европах БЭМ юзают.


        1. Spunreal
          10.06.2022 08:43

          Ну, например, это использует БЭМ -- https://github.com/material-components/material-components-web


  1. virtusha
    09.06.2022 00:23
    -1

    Чтобы узнать, что такое БЭМ, мне пришлось выделить аббревиатуру, нажать Контрол+С, потом открыть новую вкладку и в строку поиска вставить скопированный объект, потом нажать Энтэр... Блин, ну неужели нельзя сразу в начале статьи написать одно предложение, которое бы пояснило, что такое этот БЭМ?

    Подробнее почитать о методологии можно здесь.

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


    1. Semapl3 Автор
      09.06.2022 07:09

      Простите за доставленные неудобства.
      Для вас специально нашел старую, но в рамках БЭМ актуальную статью
      "Верстка для самых маленьких. Верстаем страницу по БЭМу"


  1. Zoolander
    09.06.2022 07:28

    Как я вижу, БЭМ возникла на том этапе фронтенда, когда стили всех компонентов существовали в одном пространстве, в одном документе. Да и сами компоненты поддерживались за счет усилий разработчиков, то есть за счет неких договоренностей о нейминге.

    В современном фронтенде, с использованием фреймворков, которые сразу дают компонентный подход из коробки в сочетании с CSS modules - все компоненты и стили и без особых усилий имеют разные префиксы и не влияют друг на друга.

    В Flutter нет проблемы пересечения селекторов или правил CSS, поэтому говорить о БЭМ в целом не имеет смысла.

    Но есть смысл подумать о нейминге, чтобы разделять общие классы и классы, которые используются только внутри какой-то страницы.

    Это не обязательно делать за счет подчеркиваний и других пробелов.

    Подчеркивания и пробелы возникли в БЭМ только потому, что "All CSS syntax is case-insensitive within the ASCII range... ", то есть camelCase был опасен, так как регистр буквы не учитывался.

    В Flutter вы можете смело перейти к camelCase, как и в любом другом случае, когда вам не нужно использовать CSS-правила или пути к директориям (Unix хорошо различает разные регистры в директориях, а вот Windows нет - папка camel и CAMEL там одно и то же)


    1. Semapl3 Автор
      09.06.2022 07:43

      Да все верно, Flutter лишен тех проблем для которых изначально создавался БЭМ

      Разделять да нужно, я долго размышлял на эту тему как это разделить, чтобы это было наглядно, и пока что называть Name__element_modificator для меня кажется вполне удачным.

      Если есть идеи лучше чем подчеркивания можно обсудить и сравнить


  1. Nick2022
    09.06.2022 07:32

    С принципом разделения виджетов на виджеты я согласен, но только с организационной точки зрения. Аргументация про производительность не верна:

    константы лишний раз не перерисовываются в отличие от функции

    • Виджеты-константы перерисовываются так же, как и обычные виджеты. При каждом их использовании создаются такие же узлы и в element tree, и в render tree . Их плюс в том, что они не пересоздаются каждый раз при перестроении дерева виджетов. Чтобы виджет лишний раз не перерисовывался, используют RepaintBoundary.

    • Функции могут возвращать константный виджет, который не будет пересоздаваться при каждом ее вызове. Достаточно использовать ключевое слово const перед вызовом const конструктора. Это стоит делать и в build методах.

    • При замене функции или метода на константный виджет, мы меняем вызов функции или метода на вызов build метода константного виджета фреймворком. Результат этого метода может быть возвращать как константный, так и обычный объект => сама по себе замена функции виджетом не повлияет на производительность в лучшую сторону.


  1. Mitai
    09.06.2022 10:29
    -2

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


    1. Neikist
      09.06.2022 10:43

      Не понимаю такого фанатизма. У любой технологии есть недостатки и есть смысл смотреть/искать разные подходы по их преодолению.


      1. Mitai
        09.06.2022 15:55
        +1

        при чем тут фанатизм, я просто не вижу какие проблемы или недостатки БЭМ тут исправил, этот блок элемент модификатор был для чего? для веба а почему? да потому что они там не могли сами придти к какому то единому стандарту, у нас вроде нет с этим проблем, переиспользуемость, в флаттере идет в комплекте создали виджет в файл вынесли где надо вызвали, что может быть проще?
        взять технологию которая и в вебе то уже давно умерла и притащить в флаттер где оно не надо от слова совсем вот что такое фанатизм


        1. Neikist
          09.06.2022 16:14

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