Lesson 45 of 83 intermediate

Gradle Basics, Dependencies, Build Variants & Product Flavors

Control your entire build pipeline — version catalogs, flavors, and buildConfigField are daily senior tools

Open interactive version (quiz + challenge)

Real-world analogy

Gradle's build variants are like a restaurant's menu customization system. The same kitchen (your source code) can produce a 'dev meal' with extra debug sauce and verbose logging, a 'staging meal' that looks almost production-ready for QA reviewers, and a 'prod meal' with perfect presentation and no debug garnishes. The kitchen instructions (build.gradle.kts) specify exactly what goes into each variant.

What is it?

Gradle is Android's build system that compiles source code, packages resources, runs tests, and produces signed APKs or AABs. Build variants (the combination of buildTypes and productFlavors) let one codebase produce multiple distinct app variants for different environments, audiences, or monetization strategies — all from a single build command.

Real-world relevance

A SaaS school management platform maintains three productFlavors: dev (points to localhost API, has debug menu), staging (points to staging API, has internal QA tools visible), and prod (points to production API, all debug code stripped by R8). Each flavor is installable side-by-side on a QA device using applicationIdSuffix. The CI pipeline builds all 6 variants on every PR using the version catalog to guarantee dependency consistency.

Key points

Code example

// gradle/libs.versions.toml
[versions]
kotlin = "2.0.0"
agp = "8.4.0"
retrofit = "2.11.0"
hilt = "2.51"
compose-bom = "2024.05.00"

[libraries]
retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }

[bundles]
retrofit = ["retrofit-core", "retrofit-gson"]

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }

// app/build.gradle.kts
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.hilt)
    id("kotlin-kapt")
}

android {
    compileSdk = 35
    defaultConfig {
        applicationId = "com.teamzlab.schoolapp"
        minSdk = 26
        targetSdk = 35
        versionCode = 42
        versionName = "2.7.1"
    }

    flavorDimensions += "environment"

    productFlavors {
        create("dev") {
            dimension = "environment"
            applicationIdSuffix = ".dev"
            versionNameSuffix = "-DEV"
            buildConfigField("String", "API_URL", "\"https://api.dev.school.com\"")
            buildConfigField("Boolean", "SHOW_DEBUG_MENU", "true")
        }
        create("staging") {
            dimension = "environment"
            applicationIdSuffix = ".staging"
            versionNameSuffix = "-STAGING"
            buildConfigField("String", "API_URL", "\"https://api.staging.school.com\"")
            buildConfigField("Boolean", "SHOW_DEBUG_MENU", "false")
        }
        create("prod") {
            dimension = "environment"
            buildConfigField("String", "API_URL", "\"https://api.school.com\"")
            buildConfigField("Boolean", "SHOW_DEBUG_MENU", "false")
        }
    }

    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
        debug {
            isDebuggable = true
            isMinifyEnabled = false
        }
    }

    buildFeatures { buildConfig = true; compose = true }
}

dependencies {
    implementation(libs.bundles.retrofit)
    implementation(libs.hilt.android)
    kapt(libs.hilt.compiler)
    val composeBom = platform(libs.compose.bom)
    implementation(composeBom)
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.material3:material3")
    debugImplementation("androidx.compose.ui:ui-tooling")  // Only in debug
}

Line-by-line walkthrough

  1. 1. libs.versions.toml [versions] table — single source of truth for version strings; changing 'retrofit = 2.11.0' here updates every module that references version.ref = 'retrofit'.
  2. 2. [libraries] section — defines library coordinates (group:artifact) linked to a version reference; generates a type-safe accessor (libs.retrofit.core) usable in any build.gradle.kts.
  3. 3. [bundles] retrofit — groups retrofit-core and retrofit-gson; any module calling implementation(libs.bundles.retrofit) gets both transitive dependencies in one line.
  4. 4. flavorDimensions += 'environment' — declares a dimension named 'environment'; every productFlavor must specify which dimension it belongs to or Gradle will error.
  5. 5. applicationIdSuffix = '.dev' in the dev flavor — the final applicationId becomes com.teamzlab.schoolapp.dev; this is a different app identity from prod, allowing parallel installation.
  6. 6. buildConfigField('String', 'API_URL', '"https://api.dev.school.com"') — generates public static final String API_URL = ... in BuildConfig; the escaped quotes inside the string literal are required for valid Java string generation.
  7. 7. isMinifyEnabled = true in release — activates R8 which shrinks unused classes, obfuscates names, and optimizes bytecode; essential for APK size and reverse-engineering protection in a fintech app.
  8. 8. proguardFiles(getDefaultProguardFile(...), 'proguard-rules.pro') — applies Google's baseline rules then your custom rules; keep rules for Retrofit models and Gson serialization are typically needed here.
  9. 9. debugImplementation('androidx.compose.ui:ui-tooling') — this dependency is ONLY included in debug builds; it provides Compose Preview support and layout inspector but adds size that must not reach production.
  10. 10. platform(libs.compose.bom) — a BOM (Bill of Materials) aligns all Compose library versions to a tested-compatible set; no individual Compose version strings are needed after this.

Spot the bug

// app/build.gradle.kts — SaaS school app
android {
    flavorDimensions += "environment"

    productFlavors {
        create("dev") {
            buildConfigField("String", "API_URL", "https://api.dev.school.com")  // Bug 1
            applicationIdSuffix = "dev"  // Bug 2
        }
        create("prod") {
            buildConfigField("String", "API_URL", "\"https://api.school.com\"")
        }
    }

    buildTypes {
        release {
            isMinifyEnabled = false  // Bug 3
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
        }
    }

    buildFeatures {
        buildConfig = false  // Bug 4
    }
}

dependencies {
    implementation("com.squareup.retrofit2:retrofit:2.9.0")  // Bug 5
    implementation("com.squareup.retrofit2:retrofit:2.11.0")
}
Need a hint?
Check the string quoting in buildConfigField, the applicationIdSuffix format, minification setting, buildConfig feature flag, and the duplicate dependency.
Show answer
Bug 1: buildConfigField('String', 'API_URL', 'https://api.dev.school.com') — the value is missing surrounding escaped quotes. BuildConfig generates Java code, so the value must be a valid Java string literal. It needs to be '\"https://api.dev.school.com\"' (with escaped double quotes inside the Kotlin string). Without them, the generated BuildConfig.java will have API_URL = https://api.dev.school.com (unquoted), which is a compile error. Bug 2: applicationIdSuffix = 'dev' — the suffix should start with a dot: applicationIdSuffix = '.dev'. Without the leading dot, the applicationId becomes com.teamzlabschoolappdev (no separator) instead of com.teamzlab.schoolapp.dev, which is invalid and will cause a build error or produce an unexpected package name. Bug 3: isMinifyEnabled = false in release — the release build type for a production SaaS app should have isMinifyEnabled = true. Without R8, the APK is large, slow to download, and trivially reverse-engineered. This is a critical security and performance oversight, especially for a school management platform handling PII. Bug 4: buildConfig = false — this disables generation of the BuildConfig class entirely. Since the productFlavors are using buildConfigField() to inject API_URL, those fields have nowhere to go — Gradle will error or silently discard them. Fix: remove this line or set it to true. Bug 5: Duplicate Retrofit dependency with conflicting versions — implementation is called twice for retrofit with versions 2.9.0 and 2.11.0. Gradle will resolve this to the higher version (2.11.0) but will emit a warning and the behavior may differ across Gradle versions. Fix: remove the 2.9.0 line and use only 2.11.0, ideally via a Version Catalog entry to avoid this class of mistake entirely.

Explain like I'm 5

Imagine you are baking cookies for three different events: a test batch for yourself (dev), a batch for friends to try before the party (staging), and the real party batch (prod). The recipe is the same but you add different decorations and toppings for each batch. Gradle productFlavors do exactly this for your app — same code, different settings for each 'event'.

Fun fact

Before Version Catalogs, large Android projects had dozens of Ext blocks in root build.gradle with hundreds of version strings. Googlers surveyed internally and found developers spent up to 15% of their time resolving dependency version conflicts. Version Catalogs, introduced in Gradle 7.0, cut that to near zero for teams that adopt them.

Hands-on challenge

Set up a build.gradle.kts for a fintech Android app with: (1) a Version Catalog entry for OkHttp 4.12.0 and a 'networking' bundle; (2) three productFlavors: sandbox, uat, production — each with a different API_URL buildConfigField and applicationIdSuffix for sandbox and uat; (3) a release buildType with minification enabled and a custom ProGuard file; (4) a debugImplementation dependency on LeakCanary. Write the complete build.gradle.kts android{} and dependencies{} blocks.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery