Привет, меня зовут Никита Чернобрисов, и я делаю 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-разработчицы Полины Скалкиной.
Комментарии (11)
dimentiyinfo
12.07.2023 12:59Спасибо за текст
Да, при добавлении новой иконки у нас все еще будут заново собираться
модули всех фичей, но это не будет происходить в каждом тикете. А вот
при условном изменении цвета текста в нашем кастомном тулбаре
пересоберется только одна фичаКажется, вероятность добавления иконки сильно больше переделки существующего используемого стиля, у вас не так?
Представляется, что если волатильность брендбука начинает влиять на время сборки - нужно сам брендбук делить на несколько. Всё-таки это некая система отсчёта, которая не должна меняться постоянно.
Кажется в действительно большом проекте с общим "брендбуком", с постоянными добавлениями условных иконок в нём, можно заводить раз в квартал модуль для нововведений и также раз в квартал делать чисто технические изменения по переносу накопленного в модуль брендбука. Пересборок будет минимум, ведь новые иконки обычно для новых фичnchernobrisov Автор
12.07.2023 12:59Спасибо за прочтение)
Тут скорее имеется в виду небольшие изменения в кастомных вью, например, у тулбара был заголовок, а в новой фиче появился еще подзаголовок, и такое у нас случалось довольно часто. Собственно поэтому мы и вынесли компоненты в отдельные модули.
С иконками же поступали следующим образом: дизайнеры шли впереди на 1-2 фичи, и когда разработчик принимался за фичу с еще не добавленными иконками, то автоэкспортом выгружал пачкой все новые иконки. Таким образом, брендбук вызывал только по одной дополнительной пересборке у каждого разработчика на несколько фич.
Если же несколько разработчиков начинали параллельно работать с новыми фичами, в которых есть новые компоненты брендбука, то кто-то один делал экспорт и сразу делал пулл реквест, который мы быстро мержили.
Для реально большого проекта идея с разделением брендбука выглядит рабочей.
quaer
То есть яндекс не сам пишет свои приложения? Кому принадлежат права?
Doubleserj
Почти все большие компании пользуются услугами внешних подрядчиков в разных форматах. Все права принадлежат заказчику, разумеется.
quaer
Интересно, приложение на маркете опубликовано от имени "Intertech Services AG". В инете пишут, что "Приложения "Яндекса" в Google App Store ещё в марте 2022-го разделились на две части. В одной разработчиком, как и раньше, указана компания "Яндекс", а в другой — Intertech Services AG."
А пишет приложения кто-то третий. Интересно девки пляшут оказывается.
Зачем народ тогда в яндекс нанимается?
Doubleserj
В данном кейсе у нас приложение было написано смешанной командой, а продуктовая часть и бекенд были целиком на стороне клиенте.
Но скоро у нас выйдет подкаст на ютуб-канале Doubletapp с CTO Яндекс Путешествий, в нём мы подробно разобрали то, почему и в каких случаях большие компании пользуются услугами внешних разработчиков.
quaer
Что такое продуктовая часть и бекенд? Клиент это кто/что (в смысле тонкий клиент или клиент в смысле исполнитель)? Если клиент это исполнитель, то если он писал всё целиком, то что такое смешанная команда?
Надеюсь, на канале вы изъяетесь более понятным языком. Также интересно как выбирается подрядчик. Почему выбрали вашу компанию?
Doubleserj
Клиент = наш клиент, тот кто делает заказ, в данном случае Яндекс.Путешествия.
У сложных продуктов есть почти серверная часть, она есть и Путешествий, на ней работает и веб-портал https://travel.yandex.ru/avia/. И есть продукт-менеджеры, которые отвечают за то, как продукт выглядит и работает: какой функционал, какие фичи и т.д.
Смешанная команда: в команде были и наши ребята, и представители Яндекса. Мы начинали проект целиком нашей командой мобильной разработки и тестирования, потом подключили яндексовых тимлидов. И всё происходило в процессах заказчика.
У нас уже был большой опыт работы с разными сервисами Яндекса как с клиентами, мы уже сотрудничали с Путешествиями по другим проектам, доказали компетентность, к нам было доверия со стороны технического руководства, поэтому выбрали нас. В целом в таких случаях для выбора проводится тендер, и рассматриваются факторы опыта, цены, предыдущего опыта, наличие необходимых ресурсов прямо сейчас.
Почему это делают большие компании? Всегда нехватает своих людей, быстро нанять большую качественную команду очень сложно, особенно когда к людям выставляются высокие требования (все участники проекта с нашей стороны проходили собеседование со стороной заказчика на предмет хард скиллов). Также не всегда для экспериментальных продуктов имеет смысл нанимать себе людей, а можно в начале проэкспериментировать силами сторонней команды, а по итогам уже смотреть.
quaer
То есть ваша задача была создать интерфейс пользователя для Андроид?
И он получился многомодульным? Сколько примерно разных экранов?
Doubleserj
Мы делали мобильные приложения под Android и iOS. Разработку и тестирование мобильной части.
Если интересно ещё про iOS — этой весной прокатили по всем крупным конфам, где была мобильная секция, доклад о том, как мы использовали SwifUI в iOS-приложении Путешествий и какие проблемы с этим были. Можно посмотреть запись https://habr.com/ru/companies/doubletapp/news/740952/
В Android-приложении, на момент написания статьи, ~20 экранов и более 60 модулей
quaer
По 3 самописных вьюхи на экран?