Привет, меня зовут Никита Чернобрисов, и я делаю Android-приложения в Doubletapp. Полтора года назад мы начали работать над приложением «Яндекс Путешествий» — само приложение доступно в Play Store, а про кейс подробно можно прочитать тут. При старте у нас возникло много архитектурных холиваров, в частности о том, как хранить ресурсы и пользоваться ими. И, как это и заведено, первые решения оказались неудачными. Я расскажу вам, дорогие читатели, удары каких граблей оставили больше следов и к чему мы пришли.

Для кого эта статья?

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

  • любое минорное изменение в ресурсах вызывает пересборку всего проекта,

  • приходится постоянно ходить в чат к команде и спрашивать, добавлял ли кто-то нужную тебе иконку,

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

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

  • вам просто что-то не нравится в том, как у вас хранятся ресурсы, и вы хотите это улучшить :)

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

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

Фигма тут.

Репозиторий с проектом тут.

Начинаем

Итак, к нам пришел «заказчик / менеджер / кто-то, дающий нам задачи» и сказал, что надо разработать приложение с десятком разных фич, а какие-то отдельные фичи мы хотим в будущем встраивать в другие наши проекты. Дизайнеры уже набросали макеты приоритетных фичей. Мы как осознанные разработчики не ленимся и сразу начинаем придумывать многомодульную архитектуру, дабы в будущем и нам, и потомкам было проще с этой махиной работать. Мы набросали, по какому контракту будут взаимодействовать модули между собой, как будут ходить запросы в сеть и тому подобное, и в конце концов пришли к вопросу, что мы делаем с нашими ресурсами — иконками, картинками, цветами, текстами.

Для удобства словосочетание «устройство хранения ресурсов приложения» мы в команде называем «брендбуком», так же будем и в статье. Теперь решаем, какие цели должен выполнять брендбук:

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

  • редактирование брендбука не вызывает долгую пересборку большей части проекта,

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

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

Договор с дизайнерами

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

Итак, два скриншота: на первом — фигма курильщика, на втором — дизайнера.

Фигма курильщика: текст вставлен без привязки к стилю, мы видим все его параметры в виде размера, цвета и так далее
Фигма курильщика: текст вставлен без привязки к стилю, мы видим все его параметры в виде размера, цвета и так далее
Фигма дизайнера: к тексту применен вынесенный в компоненты стиль, нам важен не каждый его параметр, а имя стиля, касается и цвета
Фигма дизайнера: к тексту применен вынесенный в компоненты стиль, нам важен не каждый его параметр, а имя стиля; касается и цвета

В первом варианте мы видим, что дизайнер не подумал, какие типы текстов будут у нас в приложении, и нарисовал на глаз. Такой подход приведет к потере консистентности оформления текста в разных частях приложения, и нам как разработчикам придется постоянно проверять каждый параметр текста при верстке. Мы этого не хотим и просим привести фигму в порядок, чтобы она выглядела, как на втором скриншоте: красота, мы видим имена стиля и цвета и при верстке задаем их, ведь они уже будут лежать у нас в проекте (про то, как и куда класть — читать дальше).

Это правило работает не только для текстов, но и для цветов, иконок и иллюстраций — просим дизайнера провести такую работу. А далее начинается следующий этап приготовлений в фигме.

Дизайн-система (пригодная для дальнейшего автоэкспорта)

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

Как выглядит минимальная дизайн-система в фигме
Как выглядит минимальная дизайн-система в фигме

Фрейм colors включает в себя перечисление всех цветов (кстати, значения некоторых цветов могут дублироваться и это нормально: дизайн-команда сможет в будущем изменить цвет группы элементов, не задевая другие), используемых в дизайне, typography — стили текста, illustrations — сложные картинки, которые мы не сможем конвертировать из svg в vector drawable, icons — иконки, векторы которых, напротив, прекрасно конвертируются студией, и components — более сложные компоненты, например, кнопки или элементы, которые будут у нас представлены как кастомные вьюшки. Также здесь могут быть перечислены другие описания интерфейса, например, скорость анимаций, цвета, углы градиентов и тому подобное.

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

Первый вариант, который не стоит повторять

Как уже говорилось, элементы брендбука должны быть доступны всем модулям, которые работают с отображением интерфейса. Разумеется, мы приходим к тому, что брендбук будет отдельным модулем, и он в дальнейшем будет импортироваться в другие модули. Добавляем в проект цвета, иконки, иллюстрации в разных размерах, делаем кастомные вьюшки для кнопок. Для удобства в дальнейшем сопоставлении цветов в фигме и в проекте стоит сохранить те нейминги, которые дали дизайнеры — так не придется каждый раз вспоминать, под каким именем у нас хранится та или иная иконка. Я бы рекомендовал только добавить уникальный для проекта префикс или суффикс в название. Это спасет нас от ситуации, когда нужно будет подключить отдельную фичу в другой проект, но там уже будет храниться ресурс с таким же именем (в любом проекте найдется иконка с названием ic_arrow или цвет с именем background). Так будет выглядеть объявление цветов приложения:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color
    name="fifty_two_challenge_app_text_primary">#000000</color>
    <color
    name="fifty_two_challenge_app_text_inverse">#FFFFFF</color>
    <color
    name="fifty_two_challenge_app_text_control_secondary">#747474</color>
    <color
    name="fifty_two_challenge_app_control">#71DE9D</color>
    <color
    name="fifty_two_challenge_app_background_main">#D6DED9</color>
    <color
    name="fifty_two_challenge_app_background_secondary">#FFFFFF</color>
    <color
    name="fifty_two_challenge_app_control_pressed">#4FC982</color>
    <color
    name="fifty_two_challenge_app_control_disabled">#AFAFAF</color>
    <color
    name="fifty_two_challenge_app_control_highlighted">#F3F3F1</color>
</resources>

Такой метод хранения ресурсов малозатратный в плане времени на реализацию и на первых этапах проекта будет прекрасно работать. Если есть стопроцентная уверенность в том, что проект никогда не вырастет из 3-4 модулей, то тут можно и остановиться. Но я уверенно скажу, что кто-то из разработчиков повесится, когда у вас будет 100+ модулей, ведь сборка проекта будет занимать время, сравнимое с продолжительностью полета к альфе Центавра. Давайте разберемся почему. Нарисуем схему зависимостей небольшого проекта, состоящего из 6 модулей:

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

Дробим наш монолит

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

  • Во-первых, такие константы, как цвета или иконки, меняются крайне редко, при этом каждый из экранов их использует.

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

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

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

Figma автоэкспорт

Добавим еще один элемент тюнинга брендбука — автоэкспорт ресурсов. Для этого воспользуемся утилитой FigmaExport. Мы кратко пробежимся по алгоритму ее подключения в наш проект. Стоит сразу оговориться: утилита использует FigmaApi, который требует вынесения скачиваемых компонентов в библиотеки. Функционал экспорта платный, поэтому ваша команда должна иметь оплаченную подписку.

Подготовка в Figma

Мы не просто так просили коллег-дизайнеров создавать отдельные компоненты для цветов, текстов и изображений и размещать их на выделенных фреймах. Это необходимое условие для работы FigmaExport. Если эта работа была проделана, то остался последний шаг — публикация компонентов в командную библиотеку, общее хранилище стилей, чтобы предоставить к ним доступ извне. Для этого щелкаем на каждый из фреймов правой кнопкой и жмем Publish selected components. Готово, идем в студию!

Подключаем FigmaExport в проект

Оформим эту часть, как инструкцию с шагами:

  1. Cкачиваем последнюю версию исходников отсюда.

  2. Достаем содержимое архива и кладем прямо в наш модуль core/brandbook рядом с директорией src, структура модуля будет выглядеть так:

Untitled
  1. Опционально можно удалить папку figma-export_XcodeExport.bundle, она нам не потребуется.

  2. На скрине можно заметить файл figma-export.yaml — это конфигурация для запуска экспорта, его нам нужно создать самим. Готовый пример файла можно найти тут, а полный перечень параметров — тут. Мы разберем минимальную рабочую версию и посмотрим основные параметры файла

    • figma-export.yaml

      figma:
        lightFileId: 75cKsK1wB2obpck3TfevLb
      common:
      colors:
      nameValidateRegexp: '^([a-zA-Z_]+)1_color'
      typography:
      nameValidateRegexp: '^([a-zA-Z_]+)1_style'
      images:
      nameValidateRegexp: '^([a-zA-Z_]+)1'
      icons:
      nameValidateRegexp: '^([a-zA-Z_]+)1'
      android:
      mainRes: './src/main/res'
      icons:
      figmaFrameName: Icons
      output: "icons"
      images:
      figmaFrameName: Illustrations
      format: webp
      output: "images"
      scales: [1, 1.5, 2, 3, 4]
      webpOptions:
      encoding: lossy
      quality: 90
      

    lightFileId — id страницы в фигме, его можно достать из url. Открываем фигму и копируем текст после file и до названия проекта. Если приложение поддерживает темную тему, то используем darkFileId и сделаем то же самое;

Untitled

nameValidateRegexp регулярное выражение, валидирующее имя ресурса, если в названии будет несовпадение с регуляркой, то мы об этом узнаем в терминале;

nameReplaceRegexp — как я писал выше, мы хотим добавлять префикс в именования ресурсов в проекте, тут мы и используем этот параметр. Укажем fifty_two_challenge_app_$1_color и text_primary превратится в fifty_two_challenge_app_text_primary_color;

В секции android мы прописываем, где находится директория для хранения ресурсов mainRes, далее для иконок и иллюстраций мы указываем output — директории, куда их сохранять относительно mainRes. Отдельно для иллюстраций задаем такие параметры, как format — формат, в котором должен происходить экспорт, например webp*,* и требуемые размеры scales, которые могут принимать значения: 1 (mdpi), 1.5 (hdpi), 2 (xhdpi), 3 (xxhdpi), 4 (xxxhdpi).

  1. Осталось добавить наш фигма-токен, чтобы FigmaExport получил доступ к макетам. Для этого идем в настройки профиля figma, находим пункт Personal access tokens, копируем и добавляем в переменные среды командой:

    export FIGMA_PERSONAL_TOKEN=[место_для_вашего_токена]

  2. Поехали! Для запуска выполняем команды в терминале, и ресурсы из фигмы прилетят прямо в проект!

    # Экспорт иконок
    ./figma-export/Release/figma-export icons -i figma-export.yaml
    
    # Экспорт изображений
    ./figma-export/Release/figma-export images -i figma-export.yaml
    
    # Экпорт стилей текста
    ./figma-export/Release/figma-export typography -i figma-export.yaml
    
    # Экспорт цветов
    ./figma-export/Release/figma-export colors -i figma-export.yaml
    

Подводим итоги

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

По мотивам приключений нашей команды я бы выделил такие тезисы:

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

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

  • если у вас нет автоэкспорта, обговорите в команде, кто, как и когда добавляет новые ресурсы, как вы будете именовать ресурсы, где вы будете их хранить. Увидев какой-то компонент в фигме, разработчик не должен тратить 10 минут на попытки понять, есть ли он уже в проекте или нет;

  • удобная фигма — ответственность не только дизайнера, все предлагайте свои улучшения — коллеги это оценят.

Еще о проекте:

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


  1. quaer
    12.07.2023 12:59

    я делаю Android-приложения в Doubletapp. Полтора года назад мы начали работать над приложением «Яндекс Путешествий»

    То есть яндекс не сам пишет свои приложения? Кому принадлежат права?


    1. Doubleserj
      12.07.2023 12:59

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


      1. quaer
        12.07.2023 12:59
        -1

        Интересно, приложение на маркете опубликовано от имени "Intertech Services AG". В инете пишут, что "Приложения "Яндекса" в Google App Store ещё в марте 2022-го разделились на две части. В одной разработчиком, как и раньше, указана компания "Яндекс", а в другой — Intertech Services AG."

        А пишет приложения кто-то третий. Интересно девки пляшут оказывается.

        Зачем народ тогда в яндекс нанимается?


        1. Doubleserj
          12.07.2023 12:59

          В данном кейсе у нас приложение было написано смешанной командой, а продуктовая часть и бекенд были целиком на стороне клиенте.

          Но скоро у нас выйдет подкаст на ютуб-канале Doubletapp с CTO Яндекс Путешествий, в нём мы подробно разобрали то, почему и в каких случаях большие компании пользуются услугами внешних разработчиков.


          1. quaer
            12.07.2023 12:59

            В данном кейсе у нас приложение было написано смешанной командой, а продуктовая часть и бекенд были целиком на стороне клиенте.

            Что такое продуктовая часть и бекенд? Клиент это кто/что (в смысле тонкий клиент или клиент в смысле исполнитель)? Если клиент это исполнитель, то если он писал всё целиком, то что такое смешанная команда?

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


            1. Doubleserj
              12.07.2023 12:59

              Клиент = наш клиент, тот кто делает заказ, в данном случае Яндекс.Путешествия.

              У сложных продуктов есть почти серверная часть, она есть и Путешествий, на ней работает и веб-портал https://travel.yandex.ru/avia/. И есть продукт-менеджеры, которые отвечают за то, как продукт выглядит и работает: какой функционал, какие фичи и т.д.

              Смешанная команда: в команде были и наши ребята, и представители Яндекса. Мы начинали проект целиком нашей командой мобильной разработки и тестирования, потом подключили яндексовых тимлидов. И всё происходило в процессах заказчика.

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

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


              1. quaer
                12.07.2023 12:59

                То есть ваша задача была создать интерфейс пользователя для Андроид?

                И он получился многомодульным? Сколько примерно разных экранов?


                1. Doubleserj
                  12.07.2023 12:59

                  Мы делали мобильные приложения под Android и iOS. Разработку и тестирование мобильной части.

                  Если интересно ещё про iOS — этой весной прокатили по всем крупным конфам, где была мобильная секция, доклад о том, как мы использовали SwifUI в iOS-приложении Путешествий и какие проблемы с этим были. Можно посмотреть запись https://habr.com/ru/companies/doubletapp/news/740952/

                  В Android-приложении, на момент написания статьи, ~20 экранов и более 60 модулей


                  1. quaer
                    12.07.2023 12:59

                    По 3 самописных вьюхи на экран?


  1. dimentiyinfo
    12.07.2023 12:59

    Спасибо за текст

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

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

    Представляется, что если волатильность брендбука начинает влиять на время сборки - нужно сам брендбук делить на несколько. Всё-таки это некая система отсчёта, которая не должна меняться постоянно.


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


    1. nchernobrisov Автор
      12.07.2023 12:59

      Спасибо за прочтение)

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

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

      Если же несколько разработчиков начинали параллельно работать с новыми фичами, в которых есть новые компоненты брендбука, то кто-то один делал экспорт и сразу делал пулл реквест, который мы быстро мержили.

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