Build Flavors — технология, позволяющая собирать несколько вариантов приложения с общей кодовой базой и общими ресурсами.
Причина появления статьи — запутанность и излишняя сложность для такой простой темы официальной документации. В короткий пост моего канала об Android-разработке оно не влезло, поэтому почти полноценная статья.
Зачем используют Flavors
Flavors — ваш выбор, если:
вам нужно 2 версии приложения - с базовым и расширенным функционалом;
вам нужно несколько версий приложения с разным оформлением или частично отличающимся функционалом.
Flavors — не лучший выбор, если вам нужно просто иметь несколько вариантов сборки приложения (с включенной и отключенной обфускацией, например). В этом случае смотрите в сторону Build Types (дефолтные: debug, release)
Общая концепция
Идея очень схожа с модным сейчас Kotlin Multiplatform — у вас есть общий код и ресурсы, лежащие в папке src/main, а также код и ресурсы, которые определены отдельно для каждой версии (каждого flavor).
Например
Например, у вас может быть два приложения про животных — про коней и про рыб. Функционал обоих — одна Activity, которая отображает картинку животного.
Вы создаете два Flavors - AnimalHorse и AnimalFish
В папке src/main/java будет лежать код самой Activity, которая выводит drawable с именем animal (R.drawable.animal). А в папках src/animalHorse/res/drawable и src/animalFish/res/drawable будут лежать картинки лошади и рыбы соответственно. При этом они должны иметь одинаковое имя = animal.
Когда вы собираете apk, в него кладутся ресурсы только для выбранного Flavor — остальные просто игнорируются.
Практика
Пошагово создадим и наполним два Flavors, например, animalHorse и animalFish.
Создаем Flavors
В файле build.gradle (уровня module) необходимо объявить flavorDimensions — группу, объединяющую наши flavors (обязательно):
android {
...
flavorDimensions "animals"
...
}
Дальше добавляем в блок android блок productFlavors, объявляем наши flavors и обязательно прописываем, к какой группе они относятся.
android {
...
flavorDimensions "animals"
productFlavors {
animalHorse {
dimension "animals"
}
animalFish {
dimension "animals"
}
}
...
}
Пересобираем проект (Build -> Rebuild Project).
Заходим в раздел Build -> Select Build Variants.... И видим, что появилось 4 варианта сборки:
animalHorseDebug
animalHorseRelease
animalFishDebug
animalFishRelease
То есть у нас всегда будет n = Build Types * Build Flavors вариантов сборки проекта.
Можно выбрать любой из них, после чего студия сделает rebuild, чтобы собрать проект только из тех файлов, которые относятся к выбранной версии.
Переопределяем applicationId
Во-первых, уточню, что applicationId != packageName (packageName = имя папок, в которых лежит проект. у всех flavors должен быть одинаковый packageName). Пост об этом в моем tg-канале c картинками: https://t.me/dolgo_polo_dev/45
Во-вторых, вы можете либо переопределить applicationId для каждого flavors полностью, либо можете добавить applicationIdSuffix для каждого flavor.
android {
defaultConfig {
applicationId "my.app"
}
...
flavorDimensions "animals"
productFlavors {
animalHorse {
dimension "animals"
applicationId "my.app.horse"
}
animalFish {
dimension "animals"
applicationIdSuffix ".fish"
}
}
...
}
Во втором варианте applicationId = applicationId (из блока defaultConfig) + applicationIdSuffix
Создаем разделы с кодом
На данный момент (10.12.2021) Android Studia сам не умеет создавать папки по объявленным Flavors, поэтому создадим их вручную.
p.s. не обязательно создавать папки и файлы, которые не собираетесь переопределять
Откройте структуру проекта в режиме Project.
Создайте в папке app/src папки animalHorse и animalFish.
В созданных папках создайте папки java/my/app/com (если packageName != "my.app.com", то папки внутренние папки будут называться по-другому), res/layout, res/drawable... (необязательно)
В папках animalHorse и animalFish создайте файл AndroidManifest.xml (обязательно)
Готово. Теперь вы можете писать общий код в папке src/main и разный код в папках src/animalHorse и src/animalFish.
Например, вы можете в общем коде хранить Activity, которая выводит на весь экран Fragment с именем FragmentListAnimals. А сам фрагмент определить в обоих версиях приложения отдельно — создайте класс FragmentListAnimals и в папке src/animalHorse/java/my/app/com/fragments, и в папке src/animalFish/java/my/app/com/fragments.
Названия папок и файлов (классов) и их иерархия в папках src/main/java и src/animalHorse/java и src/animalFish/java должны совпадать.
Об AndroidManifest
Как писалось выше, AndroidManifest.xml должен быть создан во всех трех папках - src/main, src/animalFish, src/animalHorse. При этом манифесты в папках src/animalFish и src/animalHorse могут быть пустые (содержать только тег <manifest/>).
Но если вам, например, нужно разрешение на доступ к камере только в одной версии приложения, то можете отредактировать манифест в нужной папке. При сборке приложения этот манифест будет смерджен с основным манифестом, лежащем в scr/main.
Правила соединения манифестов хорошо описаны тут.
Создание BuildConfig переменных
Вы можете создавать константы, которые меняются в зависимости от выбранной сборки. Для этого в gradle-файле создайте buildConfigField для обоих flavor:
android {
defaultConfig {
applicationId "my.app"
}
...
flavorDimensions "animals"
productFlavors {
animalHorse {
dimension "animals"
applicationIdSuffix ".horse"
buildConfigField "String", "URL", "\"https://animals.horse.ru\""
}
animalFish {
dimension "animals"
applicationId "my.app.fish"
buildConfigField "String", "URL", "\"https://animals.fish.ru\""
}
}
...
}
Теперь в любом месте кода вы можете сослаться на эту константу:
private fun initRetrofit() {
...
val retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.URL)
.build()
}
Советы и возможные трудности
Скорее всего, начав работать с flavors, вы увидите, насколько ваш код недостаточно разбит на классы. И вам придется провести приличное код-ревью, которое позволит вам переопределять минимум кода для каждой из версий приложения.
Очевидный фокап, словленный мной — когда будете создавать папки для каждой версии, убедитесь, что создали вложенные папки, а не одну. Если пакет называется my.app.com, то должны быть папки my -> app -> com, а не одна с названием my.app.com.
Используйте Dagger/Hilt для внедрения зависимостей. Так будет намного легче упорядочивать разные реализации для разных версий.
Полный список build-параметров, которые можно переопределить для каждого flavor, можно найти в оф. документации (например, minSdkVersion, versionCode...)
А более короткие посты про мобильную разработку с картинками можно почитать туть.
Комментарии (10)
sgrogov
25.12.2021 23:52А staging и production версии тоже через flavors делают или есть способ проще?
DolgopolovDenis Автор
25.12.2021 23:58смотря что имеете ввиду
если "версия возможно с багами" и "версия проверенная" - то это ветки гитхаба
если разные настройки сборки - то это Build Types
если разный функционал (почему-то) - то это Build Flavors
AndreySlonov
а когда это может пригодиться на практике, кроме создания множества некачественных копий однотипных игр-рулеток для play market?
вопрос без пост-иронии, хочется действительно узнать кейсы
DolgopolovDenis Автор
когда в компании\стартапе разрабатываются два приложения для разных типов пользователей
например, одно с доступом к функциям администратора и всем остальным функционалом, а второе - просто со всем остальным функционалом. в этом случае для безопасности можно всем пользователям рассылать урезанную версию, а администраторам полную
(на сколько это в реальности прибавляет безопасности - хороший вопрос)
но более частый кейс - платная и бесплатная урезанная версия
gatoazul
Например, мне нужно сделать одно и то же приложение паре разных фирм. Функционал везде одинаковый, но отличается оформление, логотипы и url доступа к данным.
IgorIngeneer
вероятно это не очень уж нужная фича, возможно например для очень старых версий андроид + более новых, в которыъ какие-то новые возможности UI задействованы, которых нет в старых версиях..
DolgopolovDenis Автор
нет нет, это совсем про другое
для написания кода под определенную версию пишутся просто проверки
а Build Flavors - именно про разработку нескольких версий, отличающихся по функционалу и дизайну (отличающихся, не по причине разницы версий андроида, а из-за потребностей бизнеса)
AlexWoodblock
Пример - приложение для доставки еды и курьеров. Используется общий код, для много чего, но, естественно, в приложении курьера не должно быть функции заказа еды, а в приложении клиента не должно быть варианта принять чей-то заказ.
Ну или похожий кейс в каршеринге - общий код, но в клиентском приложении нет функицй, которые есть в приложении для персонала, обслуживающего авто (например, пометить авто, как нерабочее и вызвать для него эвакуатор).
DolgopolovDenis Автор
интересно, это предположение или инсайт из одной из реальных известных компаний?)
kasyakby
Два как мне кажется самых частых кейса - платное/бесплатное приложение, паблишится на разные плащадки - Google Play/Huawei AppGallery.