Привет, меня зовут Никита Чернобрисов, и я делаю 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 в проект
Оформим эту часть, как инструкцию с шагами:
Cкачиваем последнюю версию исходников отсюда.
Достаем содержимое архива и кладем прямо в наш модуль
core/brandbook
рядом с директориейsrc
, структура модуля будет выглядеть так:

Опционально можно удалить папку
figma-export_XcodeExport.bundle
, она нам не потребуется.На скрине можно заметить файл 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 и сделаем то же самое;

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).
Осталось добавить наш фигма-токен, чтобы FigmaExport получил доступ к макетам. Для этого идем в настройки профиля figma, находим пункт Personal access tokens, копируем и добавляем в переменные среды командой:
export FIGMA_PERSONAL_TOKEN=[место_для_вашего_токена]
Поехали! Для запуска выполняем команды в терминале, и ресурсы из фигмы прилетят прямо в проект!
# Экспорт иконок ./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 минут на попытки понять, есть ли он уже в проекте или нет;
удобная фигма — ответственность не только дизайнера, все предлагайте свои улучшения — коллеги это оценят.
Еще о проекте:
«Яндекс Путешествия» в портфолио Doubletapp.
SwiftUI для большого B2C продукта – доклад iOS-разработчицы Полины Скалкиной.