How do you build different versions of the same app from one codebase?
You use build types, product flavors, and variants!
Let's explain the concept and dive into some examples.
What is a build type
A build type defines how the app is built.
The most common build types are:
- debug
- release
Debug is what you use during development. It usually:
- Is debuggable
- Has logging enabled
- Is signed with a debug key
- May include tools like LeakCanary
Release is what you ship to users. It usually:
- Is not debuggable
- Has code shrinking enabled
- Is signed with your real keystore
- Has logging disabled
So build types describe technical behavior.
What is a product flavor
A product flavor defines what app you are building.
Examples:
- demo vs prod, different backend servers
- free vs paid, different features
- white label apps for different clients
- staging vs production
Flavors describe business or product differences.
You can think of flavors as different personalities of your app.
What is a variant
A variant is the combination of:
One build type
One flavor from each flavor dimension
For example:
- demoDebug
- demoRelease
- prodDebug
- prodRelease
Each of those is a fully buildable and installable app.
When you click Run in Android Studio, you are running a specific variant.
Step 1: Basic Android app setup
Open your app module file:
app/build.gradle.kts
Inside the android block, you typically have something like:
android {
namespace = "com.example.variants"
compileSdk = 35
defaultConfig {
applicationId = "com.example.variants"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
}
This defines your base app configuration.
Step 2: Understanding build types in practice
Add build types inside the android block:
android {
buildTypes {
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = ".debug"
isDebuggable = true
buildConfigField("Boolean", "LOGGING_ENABLED", "true")
}
release {
isMinifyEnabled = true
isShrinkResources = true
buildConfigField("Boolean", "LOGGING_ENABLED", "false")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
What this does:
- Debug builds have a different application id, so you can install them next to release.
- Debug builds expose a constant called LOGGING_ENABLED.
- Release builds enable code shrinking.
Use the generated constant in code:
object LoggerConfig {
val loggingEnabled = BuildConfig.LOGGING_ENABLED
}
Now your app behaves differently depending on build type.
Step 3: Creating your first flavor
Now we introduce flavors.
We will create two environments:
- demo
- prod
First, define a flavor dimension:
android {
flavorDimensions += "environment"
productFlavors {
create("demo") {
dimension = "environment"
applicationIdSuffix = ".demo"
versionNameSuffix = ".demo"
buildConfigField(
"String",
"API_BASE_URL",
"\"https://api.demo.example.com\""
)
}
create("prod") {
dimension = "environment"
buildConfigField(
"String",
"API_BASE_URL",
"\"https://api.example.com\""
)
}
}
}
Now you have:
- demoDebug
- demoRelease
- prodDebug
- prodRelease
Each one has a different API base URL.
Use it in your networking layer:
object ApiConfig {
val baseUrl: String = BuildConfig.API_BASE_URL
}
No if statements required.
Step 4: Source sets and environment specific code
Android supports separate folders per flavor.
Structure example:
- app/src/main
- app/src/demo
- app/src/prod
- app/src/debug
- app/src/release
Suppose you want a developer menu only in demo.
Create:
app/src/demo/java/com/example/variants/FeatureFlags.kt
And in:
app/src/prod/java/com/example/variants/FeatureFlags.kt
object FeatureFlags {
const val showDeveloperMenu = false
}
The correct file is compiled automatically depending on variant.
This is cleaner than runtime checks.
Step 5: Multiple flavor dimensions
Now let us introduce branding.
Suppose you want:
- One codebase
- Two clients: acme and globex
Add a second dimension:
android {
flavorDimensions += listOf("environment", "brand")
productFlavors {
create("demo") { dimension = "environment" }
create("prod") { dimension = "environment" }
create("acme") {
dimension = "brand"
applicationIdSuffix = ".acme"
resValue("string", "brand_name", "Acme")
}
create("globex") {
dimension = "brand"
applicationIdSuffix = ".globex"
resValue("string", "brand_name", "Globex")
}
}
}
Now total variants:
2 environments times 2 brands times 2 build types equals 8 variants.
This is how large production apps handle white labeling.
Step 6: Variant specific dependencies
You can attach dependencies to specific variants.
dependencies {
implementation("androidx.core:core-ktx:1.13.1")
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
demoImplementation("com.jakewharton.timber:timber:5.0.1")
}
LeakCanary only in debug.
Timber only in demo builds.
This keeps production builds clean.
Top comments (0)