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
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
- build.gradle.kts vs build.gradle — Kotlin DSL (.kts) files provide type safety, IDE autocompletion, and compile-time error checking. They are the modern standard in 2026; Groovy .gradle files are legacy.
- Version Catalogs (libs.versions.toml) — Centralize all dependency versions in gradle/libs.versions.toml. Reference via libs.retrofit, libs.hilt.android, etc. No more version strings scattered across 20 build files.
- buildTypes — debug and release are the two default build types. debug has debuggable=true, minification off. release has minifyEnabled=true, R8/ProGuard shrinking, and must be signed with a release keystore.
- productFlavors — Create dev, staging, and prod flavors with different applicationId suffixes, API base URLs, and feature flags. Each flavor × buildType combination produces a build variant.
- Build variant matrix — 2 buildTypes × 3 flavors = 6 build variants: devDebug, devRelease, stagingDebug, stagingRelease, prodDebug, prodRelease. Each is an independently installable APK.
- buildConfigField — Inject values into BuildConfig.java at compile time: buildConfigField('String', 'API_URL', '"https://api.dev.school.com"'). Access via BuildConfig.API_URL in Kotlin code. Never hardcode URLs.
- flavorDimensions — Multiple dimensions (e.g., 'environment' and 'payment') allow a matrix like devFreeDebug, devPaidRelease. Each dimension is independent and combinations multiply.
- sourceSets — Each flavor and build type can have its own source directory: src/dev/java, src/prod/res. Files in flavor-specific directories override or supplement main/ files automatically.
- Dependency configurations — implementation (compile + runtime), api (transitive), compileOnly (compile only, not runtime), runtimeOnly (runtime only), testImplementation, androidTestImplementation, debugImplementation.
- Version Catalog bundles — Group related dependencies: [bundles] compose = ['compose-ui', 'compose-material3', 'compose-tooling']. Then: implementation(libs.bundles.compose) — one line for all Compose deps.
- Gradle caching & build speed — Enable Gradle build cache (org.gradle.caching=true in gradle.properties) and configuration cache to skip configuration phase on repeated builds. Critical for CI speed.
- applicationIdSuffix — Use applicationIdSuffix = '.dev' on debug/dev variants so dev and prod builds can coexist on the same device — essential for QA testers who need both installed simultaneously.
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. 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. [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. [bundles] retrofit — groups retrofit-core and retrofit-gson; any module calling implementation(libs.bundles.retrofit) gets both transitive dependencies in one line.
- 4. flavorDimensions += 'environment' — declares a dimension named 'environment'; every productFlavor must specify which dimension it belongs to or Gradle will error.
- 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. 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. 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. 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. 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. 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?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Version Catalogs — Gradle Docs (Gradle)
- Configure Build Variants — Android Developers (Android Developers)
- BuildConfig — Android Developers (Android Developers)
- Kotlin DSL for Gradle — Migration Guide (Android Developers)
- Shrink, obfuscate, and optimize — R8 (Android Developers)