惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
P
Privacy International News Feed
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
Cisco Talos Blog
Cisco Talos Blog
S
Schneier on Security
Project Zero
Project Zero
T
Threatpost
Spread Privacy
Spread Privacy
阮一峰的网络日志
阮一峰的网络日志
C
Cybersecurity and Infrastructure Security Agency CISA
AWS News Blog
AWS News Blog
H
Heimdal Security Blog
V
Visual Studio Blog
Google DeepMind News
Google DeepMind News
P
Privacy & Cybersecurity Law Blog
J
Java Code Geeks
罗磊的独立博客
博客园 - Franky
博客园 - 叶小钗
S
Security Affairs
月光博客
月光博客
Application and Cybersecurity Blog
Application and Cybersecurity Blog
The Last Watchdog
The Last Watchdog
WordPress大学
WordPress大学
人人都是产品经理
人人都是产品经理
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
A
Arctic Wolf
Cloudbric
Cloudbric
www.infosecurity-magazine.com
www.infosecurity-magazine.com
V2EX - 技术
V2EX - 技术
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LINUX DO - 最新话题
Y
Y Combinator Blog
宝玉的分享
宝玉的分享
酷 壳 – CoolShell
酷 壳 – CoolShell
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
N
News | PayPal Newsroom
Hugging Face - Blog
Hugging Face - Blog
美团技术团队
W
WeLiveSecurity
云风的 BLOG
云风的 BLOG
The Register - Security
The Register - Security
I
InfoQ
F
Fortinet All Blogs
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
Recent Announcements
Recent Announcements
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
L
Lohrmann on Cybersecurity

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Динамические product flavors в Android: когда статической конфигурации уже мало
PALiarMo · 2026-04-28 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение10 мин

Охват и читатели24

Туториал

Рано или поздно каждый Android‑разработчик сталкивается с задачей «одно приложение — много сборок»: white‑label‑решения, региональные версии, отдельные сборки для разных магазинов приложений, демо для клиентов, внутренние окружения.

Встроенный механизм product flavors в Android Gradle Plugin отлично справляется со своей задачей — пока количество вариантов умещается в голове и в паре экранов build.gradle.kts.

Когда же flavors становится много и каждый отличается не только applicationId, но и набором фич, ключами аналитики и доступностью в конкретном магазине, поддерживать всё это руками уже невозможно.

В этой статье я разберу подход, при котором конфигурация flavors строится динамически: список вариантов и их параметры живут вне build.gradle.kts.

Скрипт лишь интерпретирует внешний источник и разворачивает нужные варианты сборки.

Короткое напоминание: product flavor и build variant

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

Build type определяет базовую конфигурацию сборки: debug, release, иногда staging. Здесь живут настройки оптимизации, ProGuard/R8, подпись.

Product flavor — это версия приложения, которая отличается от других по applicationId, ресурсам, зависимостям, API‑ключам или включённым функциям. Каждый flavor обязан принадлежать какому‑то flavor dimension; если dimension в модуле один, все flavors попадают в него автоматически.

Build variant — декартово произведение всех dimensions и build types.

Если в проекте есть flavors demo и full в dimension mode, и build types debug и release — мы получим четыре варианта: demoDebug, demoRelease, fullDebug, fullRelease.

Важные свойства flavor, которые нам понадобятся:

  • Каждый flavor может задавать applicationIdSuffix или полностью переопределять applicationId

  • flavor поддерживает все свойства defaultConfig — базовые значения задаются в defaultConfig, а flavor только переопределяет нужное.

  • Dimensions можно объединять. Например, dimension vendor содержит список заказчиков, а dimension store — магазины (GooglePlay, RuStore, AppGallery). Gradle комбинирует по одному flavor из каждой dimension с каждым build type.

Если vendor содержит 10 заказчиков, store — 3 магазина, а build types два (debug и release), на выходе получаем 60 build variants.

В этот момент статическое описание становится проблематичным и болезненным

Проблемы статического описания

Классический подход выглядит так:

android {
    flavorDimensions += listOf("vendor", "store")

    productFlavors {
        create("vendorA") {
            dimension = "vendor"
            applicationId = "com.example.vendora"
            buildConfigField("String", "API_BASE_URL", "\"https://api.vendora.com\"")
            buildConfigField("boolean", "FEATURE_ANALYTICS", "true")
            // ... 
        }
        create("vendorB") {
            dimension = "vendor"
            applicationId = "com.example.vendorb"
            buildConfigField("String", "API_BASE_URL", "\"https://api.vendorb.com\"")
            buildConfigField("boolean", "FEATURE_ANALYTICS", "false")
            // ...
        }
        // и так далее на каждого заказчика
    }
}

Болевые точки, которые я встречал в реальных проектах:

  • Копипаста.

  • Хранить секреты VCS (надеюсь вы такое не делаете, но на практике, к сожалению, встречал)

  • Жёсткая связанность. Любое изменение в составе сборок требует правки build.gradle.kts и коммита в репозиторий.

  • Конфигурации для разных команд. Часто возникает ситуация что у QA, локального разработчика и CI необходимы свои комбинации фич.

  • Сложности автоматизации. CI-пайплайны, которые должны собрать «всё что доступно для RuStore», вынуждены парсить build.gradle.kts регуляркой или хардкодить список вендоров в YAML.

Идея: единый источник правды

Ключевая мысль проста: описание flavors не должно жить внутри build.gradle.kts. Сам скрипт должен быть интерпретатором внешнего списка — прочитал, сгенерировал, подставил значения.

Этот внешний источник может быть чем угодно:

  • JSON/YAML/properties-файл в корне проекта;

  • набор файлов, где каждый описывает один flavor;

  • удалённый конфиг, подтягиваемый на этапе configuration

  • комбинация первых двух: список имён flavors в репозитории (чтобы сборка была воспроизводимой), а секреты и фиче-тоглы — в .properties-файлах, закрытых gitignore и распространяемых через защищённый канал.

Последний вариант — самый практичный для большинства проектов.

Разбор ключевых техник

1. Генерация flavors из внешнего списка

[
  {
    "name": "vendorA",
    "applicationId": "com.example.vendora",
    "propertiesFile": "flavors/vendorA.properties"
  },
  {
    "name": "vendorB",
    "applicationId": "com.example.vendorb",
    "propertiesFile": "flavors/vendorB.properties"
  },
  {
    "name": "vendorC",
    "applicationId": "com.example.vendorc",
    "propertiesFile": "flavors/vendorC.properties"
  }
]

Читаем его в build.gradle.kts и генерируем flavors:

import groovy.json.JsonSlurper
import java.util.Properties

data class FlavorConfig(
    val name: String,
    val applicationId: String,
    val propertiesFile: String
)

val flavorsJson = rootProject.file("flavors.json")

@Suppress("UNCHECKED_CAST")
val flavorsList: List<FlavorConfig> = (JsonSlurper().parse(flavorsJson) as List<Map<String, String>>)
    .map { FlavorConfig(
        name = it["name"]!!,
        applicationId = it["applicationId"]!!,
        propertiesFile = it["propertiesFile"]!!
    )}

// ...

android {
    flavorDimensions += listOf("vendor", "store")

    productFlavors {
        flavorsList.forEach { config ->
            runCatching {
                create(config.name) {
                    dimension = "vendor"
                    applicationId = config.applicationId
                    versionNameSuffix = "-${config.name}"
                }
            }.onFailure { error ->
                logger.warn("\u001B[33m⚠ Не удалось создать flavor ${config.name}: ${error.message}\u001B[0m")
            }
        }

        create("GooglePlay") { dimension = "store" }
        create("RuStore")    { dimension = "store" }
    }
}

Несколько важных моментов, которые стоят мне пары отладочных дней в прошлом:

  • Безопасное чтение. runCatching вокруг create спасает от падения всей конфигурации, если один элемент списка битый. Некорректный flavor не должен ронять сборку для остальных — лучше вывести в лог ошибку и продолжить.

  • Цветной вывод в консоль. ANSI-коды очень помогают найти проблемы среди сотен строк Gradle-лога.

  • Типизация. data class FlavorConfig вместо работы напрямую с Map<String, Any> экономит часы отладки: вся дальнейшая логика обращается к типизированным полям config.name, config.applicationId, и IDE сразу подсвечивает опечатки, а не встречает их в рантайме Gradle. Приведение типов через as List<Map<String, String>> нужно только в одной точке — при парсинге.

2. Подгрузка параметров из .properties-файлов

Каждому flavor сопоставим файл с параметрами. Например, flavors/vendorA.properties:

API_BASE_URL=https://api.vendora.com
ANALYTICS_KEY=AIza...
FEATURE_ANALYTICS=true
FEATURE_PAYMENTS=false
FEATURE_CHAT=true

Для чтения properties-файла можно сделать свою хелпер-функцию что читает его и пробрасывает значения в buildConfigField:

import java.util.Properties

fun com.android.build.api.dsl.ProductFlavor.applyKeys(propertiesFile: File) {
    val props = Properties().apply {
        if (propertiesFile.exists()) {
            propertiesFile.inputStream().use { load(it) }
        } else {
            logger.warn("Файл ${propertiesFile.path} не найден, используются значения по умолчанию")
        }
    }
    // Строковые ключи
    listOf("API_BASE_URL", "ANALYTICS_KEY").forEach { key ->
        val value = props.getProperty(key, "")
        buildConfigField("String", key, "\"$value\"")
    }
    // Булевы фиче-тоглы
    listOf("FEATURE_ANALYTICS", "FEATURE_PAYMENTS", "FEATURE_CHAT").forEach { key ->
        val value = props.getProperty(key, "false").toBoolean()
        buildConfigField("boolean", key, value.toString())
    }
}

И подключаем его в цикле:

productFlavors {
    flavorsList.forEach { config ->
        create(config.name) {
            dimension = "vendor"
            applicationId = config.applicationId
            applyKeys(rootProject.file(config.propertiesFile))
        }
    }
}

Что это даёт:

  • Фиче-тоглы на уровне сборки

  • Никакой магии. Обычные константы, которые прекрасно понимает статический анализатор.

  • Секреты вне VCS

  • Безопасные значения по умолчанию

3. Несколько измерений и их комбинирование

Следующий мой вызов был в том что необходимо было делать сборки для отдельных сторов. вроде звучит просто - это делается через несколько dimensions:

android {
    flavorDimensions += listOf("vendor", "store")
    productFlavors {
        flavorsList.forEach { config ->
            create(config.name) {
                dimension = "vendor"
                // ...
            }
        }
        create("GooglePlay") {
            dimension = "store"
            buildConfigField("String", "STORE", "\"google_play\"")
        }
        create("RuStore") {
            dimension = "store"
            buildConfigField("String", "STORE", "\"rustore\"")
        }
        create("AppGallery") {
            dimension = "store"
            buildConfigField("String", "STORE", "\"appgallery\"")
        }
    }
}

но реальность такова, что не каждый заказчик выкладывается во все магазины. При 10 вендорах, 3 магазинах и 2 build types Gradle сгенерирует 60 вариантов — и в папке артефактов CI будет лежать ровно столько APK.

И вот здесь рождается отдельный класс багов. Тестировщик не должен думать, нужна ли вообще версия для AppGallery, которая автоматически собралась на CI для вендоров 1, 2, 5 и 7. Менеджер не должен писать разработчику «а почему у нас в релиз-ноутах сборка для магазина, в который мы не публикуемся». QA-команда тратит время на прогон чек-листа по сборкам, которые никто никогда не увидит. А худший сценарий — кто-то по ошибке берёт такой «фантомный» APK и отправляет его заказчику или в стор.

Решение напрашивается: невозможные комбинации должны отсутствовать в списке вариантов физически, а не лежать рядом с настоящими и ждать, пока кто-нибудь разберётся. И это подводит нас к следующей технике.

4. Условное отключение вариантов через androidComponents

Современный API androidComponents.beforeVariants позволяет влиять на variant до его создания. Это гораздо эффективнее, чем variantFilter в устаревшем API: отключённый вариант не попадает в граф задач, не занимает память и не увеличивает время Gradle Sync.

Допустим, vendorA не публикуется в RuStore, а vendorB — только в RuStore. Плюс добавим флаг enabled, чтобы можно было быстро выключить весь flavor целиком — например, пока заказчик приостановил релизы или их backend недоступен:

# flavors/vendorA.properties
ENABLED=true
STORES=GooglePlay,AppGallery

# flavors/vendorB.properties
ENABLED=true
STORES=RuStore

# flavors/vendorC.properties
ENABLED=false
STORES=GooglePlay
androidComponents {
    beforeVariants { builder ->
        val vendorName = builder.productFlavors
            .firstOrNull { it.first == "vendor" }?.second ?: return@beforeVariants
        val storeName = builder.productFlavors
            .firstOrNull { it.first == "store" }?.second ?: return@beforeVariants

        val vendorConfig = flavorsList.firstOrNull { it.name == vendorName } ?: return@beforeVariants
        val propsFile = rootProject.file(vendorConfig.propertiesFile)
        val props = Properties().apply {
            if (propsFile.exists()) propsFile.inputStream().use { load(it) }
        }

        // Vendor временно отключён целиком
        if (!props.getProperty("ENABLED", "true").toBoolean()) {
            logger.lifecycle("⊘ Отключён vendor: $vendorName (ENABLED=false)")
            builder.enable = false
            return@beforeVariants
        }
        // Vendor не публикуется в этом магазине
        val allowedStores = props.getProperty("STORES", "").split(",").map { it.trim() }
        if (storeName !in allowedStores) {
            logger.lifecycle("⊘ Отключён вариант: ${vendorName}${storeName} (магазин не в списке)")
            builder.enable = false
        }
    }
}

Ключевые выводы:

  • Фаза configuration. В этот момент variant ещё не собран, его отключение проходит безболезненно.

  • Декларативная фильтрация. Правило «этот flavor идёт только в эти магазины» записано один раз и работает для всех новых заказчиков автоматически.

  • Логируйте отключения. Иначе разработчик, не увидевший ожидаемый variant в списке, потратит полдня на поиски.

5. Связывание flavors с отдельными модулями ресурсов

Ещё один мощный приём — каждый flavor подтягивает свой модуль ресурсов или темы. Для этого в Android Gradle Plugin есть специальные суффиксы конфигураций:

// app/build.gradle.kts
dependencies {
    flavorsList.forEach { config ->
        // Префикс "vendorA" + Implementation → зависимость только для flavor vendorA
        "${config.name}Implementation"(project(":res:${config.name}"))
    }
}

Что это даёт:

  • Иерархия модулей: :res:vendorA, :res:vendorB — полностью изолированные модули ресурсов.

  • Изоляция: ресурсы одного клиента физически не попадают в APK другого.

  • Темы: строки, иконки, цвета, drawables — всё своё.

  • Масштабирование: чтобы добавить нового заказчика требуется создать модуль :res:vendorX, одна запись в flavors.json. Вносить изменения в app/build.gradle.kts не нужно.

Сценарии использования

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

White-label. Один код, десятки ребрендированных версий для разных заказчиков. Каждый vendor — отдельный flavor с уникальным набором фич, иконок и идентификаторов аналитики.

Региональные сборки. Одно приложение под разные страны с разными правовыми требованиями (GDPR vs LGPD vs 152-ФЗ), платёжными шлюзами, языками и даже функциональностью (в одном регионе есть криптокошелёк, в другом — запрещён).

Разные магазины приложений. У Google Play, RuStore, AppGallery свой биллинг, свои запрещённые библиотеки (GMS vs HMS), свои требования к аналитике. Второе измерение flavor решает эту задачу чисто.

Внутренние окружения. dev/stage/prod как flavors с разными API base URL, уровнями логирования, фиче-тоглами. В паре с external properties dev-ключи разработчика никогда не попадают в production-сборку.

A/B-тесты на уровне сборки. редкий кейс, но вполне возможен.

Корпоративные сборки. Внутренняя версия с дополнительными политиками безопасности, MDM-интеграцией, отличающимся applicationId и подписью — как отдельный flavor, не мешающий публичной сборке.

Модульная архитектура с опциональными фичами. Flavors определяют, какие feature-модули подключаются. Заказчику X нужны модули «маркет» и «аналитика», заказчику Y — только «каталог».

Быстрое отключение проблемного flavor. Заказчик приостановил релизы на время, и сборка исчезает из CI. На локальной машине разработчика всё по-прежнему собирается, если нужно.

Подводные камни

Теперь честно о том, за что заплачено временем:

  • Время configuration-фазы. Каждый readValue, чтение .properties, рефлексия в блоке productFlavors выполняются при каждом Gradle Sync (ни в коем случае не делайте в ней сетевых запросов)

  • Configuration Cache. Включённый --configuration-cache требует, чтобы все зависимости (файлы, переменные окружения) были объявлены явно через providers.fileContents() или providers.environmentVariable(). Чтение через обычный File.readText() формально работает, но ломает инкрементальную сборку и выдаёт предупреждения.

  • IDE hints и автодополнение. Android Studio может не всегда корректно определять динамически созданные flavors, часто Invalidate Caches решает проблему (в последних версиях уже не встречаю такую проблему)

  • Локализация ошибок. Если один .properties битый, сообщение Gradle не подскажет, какой именно, поэтому обязательно логируйте имя файла рядом с каждой операцией это спасёт часы отладки и облегчит вам жизнь

  • Безопасность. .properties с ключами НЕ ДОЛЖНЫ лежать в репозитории

  • Дисциплина именования. Имя flavor участвует в сотне мест: имя задачи (assembleVendorADebug), имя конфигурации (vendorAImplementation), путь к модулю (:res:vendorA), имя variant в фильтрах. Малейшая разница в регистре — и всё разваливается. Нормализуйте имена в одном месте (config.name.lowercase() или валидация на этапе чтения JSON).

  • Не переусердствуйте. Если у вас 2-3 flavors и они стабильны, статический подход по-прежнему проще и понятнее.

Вместо вывода

Никакой магии в динамических flavors нет. Это просто отделение данных от логики — ровно то, что мы делаем в коде каждый день, когда выносим конфиг в отдельный файл вместо хардкода.

И да — если у вас три flavor и они стабильны, не ломайте то, что работает.

Если в вашем проекте есть свой сценарий, который я не упомянул, — поделитесь в комментариях. Особенно интересны случаи, где источник конфигурации нестандартный