Как поддерживать тысячи приложений силами одного разработчика
Как одному разработчику поддерживать тысячи white-label мобильных приложений? Наше решение: заменили сотни схем сборки на JSON-конфигурации и автоматизацию. Единая кодовая база Flutter, модульная архитектура и универсальный CI/CD позволяют добавлять новый бренд за 2 часа.
Представьте, что вы единственный разработчик в команде, а бизнес ставит задачу поддержать тысячу мобильных приложений. Представили? Весь этот хаос с бесконечными обновлениями, багами, сборками, деплоями? А теперь медленно выдыхаем, закрываем вкладку с HH и LinkedIn, все не настолько страшно, как кажется.
Конечно же, речь идет не о тысяче уникальных приложений, а о подходе white-label, когда приложение может модифицироваться под любой бренд. Снаружи такие приложения выглядят как уникальные продукты: разный дизайн, иконки, шрифты, функциональность. Но при этом все они собраны из единой кодовой базы.
Что мы будем делать?
Чтобы понять, что мы будем делать, давайте определимся с входными требованиями:
- Кроссплатформа, для Android и iOS (мы используем Flutter)
- Увеличение количества брендов не должно увеличивать сложность добавления новых
- Ориентировочное количество брендов не менее 1000
- Удиная кодовая база, без форков, копипасты или дублирования кода
- Максимально возможная гибкость, помимо стилизации необходима кастомизация структуры навигации, фичей и прочего
- Простота добавления нового бренда, на начальном этапе не более 2 часов, в идеале прийти к no-code решению
- Универсальный CI/CD, мы не хотим менять процесс сборки после каждого изменения или добавления
Идеальный сценарий, к которому мы стремимся: клиент заходит на наш основной сайт и нажимает кнопку "Создать приложение"
- Сначала — подробная инструкция по созданию кабинетов в App Store и Google Play, чтобы получить необходимые креды, увы, это неизбежное зло
- Затем — визуальный редактор: выбор цветов, названия, иконок, сплэш-экранов, изображений, шрифтов. Клиент жмёт "Готово"
- Автоматически формируется конфигурация по контракту и сохраняется в репозитории конфигураций. Ассеты обрезаются и масштабируются. По основному брендовому цвету генерируется тема с акцентными цветами для разных элементов и состояний
- Пайплайн собирает приложение и выкладывает его в сторах
- Нужно обновление? Например, клиент хочет сменить иконку на новогоднюю со снежинками. Клиент редактирует в интерфейсе, конфигурация обновляется, пайплайн запускается и релиз уходит в продакшн

Чтобы не создавать свой велосипед, прежде чем что-то делать, нужно изучить то, что уже есть, порадоваться, и сделать свой вариант. Нативные платформы предлагают подобные механизмы, это flavors для Android и targets для iOS. Если вы еще не знакомы с этими концепциями, можно почитать доку тут и тут. Почитав доку и проведя ряд экспериментов, понимаем, что эти инструменты прекрасны, но только если количество вариантов сборки небольшое. Если же количество переваливает сотню (некомфортно уже после пары десятков), или тысячу, или мы добавляем flavors/targets для входящих библиотек начинается ад:
- Схемы сборки, provisioning profiles, скрипты — всё множится экспоненциально
- Сборка становится хрупкой, любая правка требует десятков изменений в разных местах
- Сложные процессы CI/CD, матрицы задач, долгие и нестабильные сборки
- Сложная поддержка, приложение определяется сборкой
А мы же помним, что у нас всего один разработчик, с такими проблемами никакого кофеина не хватит. Так что велосипеду быть. Мы заменяем сотни flavors/targets на сотни JSON-конфигураций и один скрипт автоматизации. После ресерча темы, определим как мы будем строить проект
- Выделим слой
core, в котором будут лежать сервисы и ресурсы, доступные любым модулем напрямую, через service locator или контекст - Все что можно структурировать и выделить в отдельные пакеты — выводим (HTTPS-клиент, дизайн-система, слой данных и прочее)
- Группируем фичи, создаем для них модули. Модули получают все зависимости через явный
DI(никакихgetIt), и могут обращаться к слою core напрямую - Создаем интерфейс
- Делаем генерацию тем на этапе компиляции по заданной конфигурации, генерировать будем стандартным
build_runner - Создаем интерфейс для конфигурации, все максимально жестко типизируем, нам не нужны ошибки при добавлении новых брендов
- Создаем базовые шаблоны структуры навигации, далее, при необходимости, будем их добавлять
- Создаем простую утилиту, которая получает конфигурацию, и изменяет код в нужных местах, чтобы получить нужный результат. Обязательно делаем этот процесс транзакционным, нам не нужны частично сконфигурированные сборки как при разработке, так и при сборке на прод. Утилиту пишем на dart, чтобы не повышать порог входа. Принцип простой: сначала кешируем все файлы, которые будем менять, потом идем итеративно, прочитали файл, распарсили файл, поменяли что нужно, записали файл. Если на каком-либо этапе возникает ошибка — откатываем все, уведомляем разработчика.
- Делаем универсальный CI/CD, который не нужно будет модифицировать под каждый бренд

Что нужно модифицировать, чтобы получить новое приложение:
- Android/iOS: идентификаторы, название, иконки, сплеш-скрин, схемы диплинков, прочие индивидуальные ассеты, некоторые специфические платформенные штуки, по типу mainActivity и его расположения
- Не забыть про пуши и локализацию
- Условный импорт шаблонов навигации и зависимостей
- Тему
Наш велосипед хорош, но добавляет новые риски, с которыми нужно бороться:
- Прямая модификация файлов может изменить что-то не то. Поэтому изменяем транзакционно в dry-run режиме, и уведомляем разработчика в случае проблем
- Конфигурация бренда может быть некорректной, поэтому делаем генерацию конфигурации по описанному контракту, без ручного изменения
- Зоопарк с версионированием, решаем автоматическим инкрементом версии сборки
А что если клиент захочет добавить перламутровые пуговицы? Сложно, но можно попробовать. В этом кейсе мы пошли по пути размещения слотов в определенных местах, в которые можно добавлять определенные блоки с функционалом. Получился конфигуратор внутри white-label приложения. Понятно, что нецелесообразно сразу наполнять все приложение слотами, поэтому здесь мы действуем итеративно по запросам клиентов.
Что мы имеем в итоге?
У нас единая кодовая база, модули атомарны, DI явный, мы можем легко добавлять и убирать фичи. У нас единый пайплайн, и единая точка входа внесения изменений. Мы легко можем масштабировать количество брендов, не увеличивая сложность этого процесса. Низкий порог входа для конфигурирования нового бренда. Разработчики требуются только для создания новых шаблонов, фичей. Тестирование можно проводить по-модульно, и отдельно тестировать сборки с имеющимися шаблонами. Нет необходимости тестировать каждое приложение.
Трудозатраты для сборки или обновления приложения минимальны. В будущем — полный no-code интерфейс для клиентов и автоматизированный релизный цикл.