Gradle: Flavors & Variants
The Gradle Basics lesson covered app/build.gradle as a single, fixed configuration. Real projects usually need more than one variant of the same app — for example, a free version of RecipeBox with ads and a limited recipe count, alongside a paid "RecipeBox Pro" edition. Product flavors and build types are Gradle's mechanism for producing multiple, differently-configured builds from a single codebase.
Build Types vs Product Flavors
These two concepts are easy to mix up because both live inside the android {} block, but they answer different questions.
| Concept | Answers | Typical values |
|---|---|---|
| Build type | "How is this compiled?" | debug, release |
| Product flavor | "Which edition/product is this?" | free, pro |
Defining Product Flavors
Flavors are grouped into a flavor dimension (a name for the axis of variation) and then listed with their own overrides.
// app/build.gradle (Groovy DSL)
android {
namespace "com.example.recipebox"
compileSdk 34
defaultConfig {
applicationId "com.example.recipebox"
minSdk 23
targetSdk 34
versionCode 1
versionName "1.0"
}
flavorDimensions "tier"
productFlavors {
free {
dimension "tier"
applicationIdSuffix ".free"
versionNameSuffix "-free"
buildConfigField "boolean", "IS_PRO", "false"
buildConfigField "int", "MAX_SAVED_RECIPES", "20"
}
pro {
dimension "tier"
applicationIdSuffix ".pro"
versionNameSuffix "-pro"
buildConfigField "boolean", "IS_PRO", "true"
buildConfigField "int", "MAX_SAVED_RECIPES", "999999"
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
applicationIdSuffix ".debug"
debuggable true
}
}
}
- flavorDimensions "tier" declares the single axis; a project with two independent axes (say, "tier" and "region") would list two dimensions and Gradle would combine one flavor from each.
- applicationIdSuffix lets free and pro install side by side on the same device as separate apps — handy for testing both at once.
- buildConfigField generates constants on the auto-created BuildConfig class, covered below.
Build Variants: Flavors × Build Types
Gradle automatically creates one build variant for every combination of flavor and build type. With two flavors and two build types, RecipeBox ends up with four buildable variants:
| Variant | applicationId | minifyEnabled | IS_PRO |
|---|---|---|---|
| freeDebug | com.example.recipebox.free.debug | false | false |
| freeRelease | com.example.recipebox.free | true | false |
| proDebug | com.example.recipebox.pro.debug | false | true |
| proRelease | com.example.recipebox.pro | true | true |
Android Studio's Build Variants panel (usually docked on the left edge) lets you pick which variant the Run button and the IDE's code analysis currently target.
Flavor-Specific Source Sets & Resources
Each flavor can have its own source set directory alongside main, containing files that only apply to that flavor — Gradle merges them with main at build time, and a flavor's file wins if both define the same resource.
app/src/
main/java/... // shared code — Recipe.java, RecipeDao.java, etc.
main/res/... // shared resources
free/res/values/strings.xml // e.g. app_name = "RecipeBox"
free/res/drawable/banner_ad_placeholder.xml
pro/res/values/strings.xml // e.g. app_name = "RecipeBox Pro"
pro/java/com/example/recipebox/pro/ProOnlyExporter.java
BuildConfig Fields as Feature Flags
Every variant generates its own BuildConfig class with the fields declared for that flavor, readable from plain Java with no extra setup.
public void saveRecipe(Recipe recipe) {
if (savedRecipeCount() >= BuildConfig.MAX_SAVED_RECIPES) {
showUpgradeToProDialog();
return;
}
recipeDao.insert(recipe);
}
public void maybeShowBanner() {
if (!BuildConfig.IS_PRO) {
adBanner.setVisibility(View.VISIBLE);
}
}
Troubleshooting
"More than one variant selected"? → Check the Build Variants panel; running a Gradle task without assembleFreeRelease/assembleProDebug naming can build every variant at once.
Flavor resource not found? → Confirm the file lives under src/<flavorName>/..., not src/main/..., and that the flavor name matches exactly (case-sensitive).
Two apps installed instead of a replace? → Expected — different applicationIdSuffix values mean different package names, so Android treats them as unrelated apps.