Build Flavors — технология, позволяющая собирать несколько вариантов приложения с общей кодовой базой и общими ресурсами.

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

Зачем используют Flavors

Flavors — ваш выбор, если:

  • вам нужно 2 версии приложения - с базовым и расширенным функционалом;

  • вам нужно несколько версий приложения с разным оформлением или частично отличающимся функционалом.

Flavors — не лучший выбор, если вам нужно просто иметь несколько вариантов сборки приложения (с включенной и отключенной обфускацией, например). В этом случае смотрите в сторону Build Types (дефолтные: debug, release)

Общая концепция

Идея очень схожа с модным сейчас Kotlin Multiplatform — у вас есть общий код и ресурсы, лежащие в папке src/main, а также код и ресурсы, которые определены отдельно для каждой версии (каждого flavor).

Структура проекта с двумя Flavor (версиями сборки) - auidioBook и eBook
Структура проекта с двумя Flavor (версиями сборки) - auidioBook и eBook
Например

Например, у вас может быть два приложения про животных — про коней и про рыб. Функционал обоих — одна 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. не обязательно создавать папки и файлы, которые не собираетесь переопределять

  1. Откройте структуру проекта в режиме Project.

  2. Создайте в папке app/src папки animalHorse и animalFish.

  3. В созданных папках создайте папки java/my/app/com (если packageName != "my.app.com", то папки внутренние папки будут называться по-другому), res/layout, res/drawable... (необязательно)

  4. В папках 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()
 }

Советы и возможные трудности

  1. Скорее всего, начав работать с flavors, вы увидите, насколько ваш код недостаточно разбит на классы. И вам придется провести приличное код-ревью, которое позволит вам переопределять минимум кода для каждой из версий приложения.

  2. Очевидный фокап, словленный мной — когда будете создавать папки для каждой версии, убедитесь, что создали вложенные папки, а не одну. Если пакет называется my.app.com, то должны быть папки my -> app -> com, а не одна с названием my.app.com.

  3. Используйте Dagger/Hilt для внедрения зависимостей. Так будет намного легче упорядочивать разные реализации для разных версий.

  4. Полный список build-параметров, которые можно переопределить для каждого flavor, можно найти в оф. документации (например, minSdkVersion, versionCode...)

А более короткие посты про мобильную разработку с картинками можно почитать туть.

Комментарии (10)


  1. AndreySlonov
    25.12.2021 02:28
    +1

    а когда это может пригодиться на практике, кроме создания множества некачественных копий однотипных игр-рулеток для play market?

    вопрос без пост-иронии, хочется действительно узнать кейсы


    1. DolgopolovDenis Автор
      25.12.2021 02:33

      когда в компании\стартапе разрабатываются два приложения для разных типов пользователей
      например, одно с доступом к функциям администратора и всем остальным функционалом, а второе - просто со всем остальным функционалом. в этом случае для безопасности можно всем пользователям рассылать урезанную версию, а администраторам полную
      (на сколько это в реальности прибавляет безопасности - хороший вопрос)

      но более частый кейс - платная и бесплатная урезанная версия


    1. gatoazul
      25.12.2021 10:30

      Например, мне нужно сделать одно и то же приложение паре разных фирм. Функционал везде одинаковый, но отличается оформление, логотипы и url доступа к данным.


    1. IgorIngeneer
      25.12.2021 12:04

      вероятно это не очень уж нужная фича, возможно например для очень старых версий андроид + более новых, в которыъ какие-то новые возможности UI задействованы, которых нет в старых версиях..


      1. DolgopolovDenis Автор
        25.12.2021 12:07

        нет нет, это совсем про другое

        для написания кода под определенную версию пишутся просто проверки

        if(android.os.Build.VERSION.SDK_INT >= n) { ... }

        а Build Flavors - именно про разработку нескольких версий, отличающихся по функционалу и дизайну (отличающихся, не по причине разницы версий андроида, а из-за потребностей бизнеса)


    1. AlexWoodblock
      27.12.2021 02:58

      Пример - приложение для доставки еды и курьеров. Используется общий код, для много чего, но, естественно, в приложении курьера не должно быть функции заказа еды, а в приложении клиента не должно быть варианта принять чей-то заказ.

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


      1. DolgopolovDenis Автор
        27.12.2021 11:50

        интересно, это предположение или инсайт из одной из реальных известных компаний?)


    1. kasyakby
      27.12.2021 14:46

      Два как мне кажется самых частых кейса - платное/бесплатное приложение, паблишится на разные плащадки - Google Play/Huawei AppGallery.


  1. sgrogov
    25.12.2021 23:52

    А staging и production версии тоже через flavors делают или есть способ проще?


    1. DolgopolovDenis Автор
      25.12.2021 23:58

      смотря что имеете ввиду

      если "версия возможно с багами" и "версия проверенная" - то это ветки гитхаба

      если разные настройки сборки - то это Build Types

      если разный функционал (почему-то) - то это Build Flavors