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.

ConceptAnswersTypical values
Build type"How is this compiled?"debug, release
Product flavor"Which edition/product is this?"free, pro
Every project already has build types even without touching Gradle — debug and release exist by default, which is why R8 only runs on the release build type in the earlier lesson. Product flavors are opt-in; a project with no flavors declared simply has none.

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
        }
    }
}
Explanation:
  • 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.
This Gradle-level free/pro split is a different mechanism from the in-app Play Billing unlock covered earlier, where a single build sells a "RecipeBox Pro" purchase at runtime. Some apps use flavors instead of billing to ship two separate listings; others combine both. Neither approach is wrong — they solve the same business problem at different layers.

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:

VariantapplicationIdminifyEnabledIS_PRO
freeDebugcom.example.recipebox.free.debugfalsefalse
freeReleasecom.example.recipebox.freetruefalse
proDebugcom.example.recipebox.pro.debugfalsetrue
proReleasecom.example.recipebox.protruetrue
flavor (free/pro) x build type (debug/release) = freeDebug, freeRelease, proDebug, proRelease

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
Explanation: Only the pro source set needs to contain ProOnlyExporter.java — a class for exporting a recipe collection to PDF, say — because it simply doesn't exist when compiling a free variant. This keeps pro-only code out of the free APK entirely, rather than shipping it disabled behind an if (IS_PRO) check.

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);
    }
}
Explanation: BuildConfig.IS_PRO and BuildConfig.MAX_SAVED_RECIPES are resolved at compile time per variant — the freeRelease build literally contains the constant 20, with no runtime lookup or network check involved.

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.

Flavors control which code and resources go into a build. The next lesson, Multidex, covers a different Gradle-adjacent limit: what happens when a single variant's code simply has too many methods to fit in one DEX file.