A Build Variant in Android development is a combination of a build type and a product flavor that determines how an APK or AAB will be built: with which parameters, resources, and code. Each build variant represents a separate Gradle configuration with its own applicationId, signing keys, and included dependencies. According to Google Android Developers, 2025, proper configuration of Build Variants reduces build time by up to 40% by excluding unnecessary resources for each variant. The build variant system is the foundation of configuration management in modern Android projects.
Key Takeaways
Build Variant is the result of combining one Build Type and one Product Flavor. If no Product Flavors are defined in the project, the Build Variant matches the Build Type. Gradle automatically generates the full set of variants as the Cartesian product of all FlavorDimensions, Product Flavors, and Build Types. For example, for free/paid flavors and debug/release types, 4 variants will be created: freeDebug, freeRelease, paidDebug, paidRelease.
Each Build Variant receives its own name in the format <Flavor><Type> with the flavor capitalized. Gradle generates separate tasks for this variant: assembleFreeDebug, installFreeDebug, bundleFreeRelease. In Android Studio, switching between variants is available through the Build Variants panel (View → Tool Windows → Build Variants). Selecting a variant affects which code is compiled, which resources are included, and which APK/AAB is produced.
The Build Variants system solves three key tasks: separating configurations for different environments (dev/staging/production), creating multiple versions of an app (free/paid), and A/B testing builds. Without Build Variants, developers would have to manually switch flags and configurations, leading to human factor errors. According to a study by Gradle Inc., 2024, implementing Build Variants reduces build errors by 60% in projects with three or more deployment environments.
AGP (Android Gradle Plugin) computes all combinations at the configuration stage. If a project has two dimensions with two and three flavors respectively, Gradle will create 2 × 2 × 3 = 12 combinations, multiplied by the number of Build Types (usually 2). Each combination gets a unique name and set of tasks. AGP automatically adds a source set for each variant: src/freeDebug/, src/paidRelease/, as well as generalized src/free/ and src/debug/. Resource reading priority: variant → flavor → type → main.
// Example: 4 Build Variants
// flavorDimensions "version", "server"
// version: demo, prod
// server: mock, live
// buildTypes: debug, release
// Total: 2 × 2 × 2 = 8 variants
android {
flavorDimensions "version", "server"
productFlavors {
demo { dimension "version" }
prod { dimension "version" }
mock { dimension "server" }
live { dimension "server" }
}
}
Build Type defines how to build the application — with or without debug information, with or without optimization, with which signing. Product Flavor defines what to build — which version of the product. Build Type is a build mechanism (debug, release, staging). Product Flavor is a product variant (free, paid, enterprise, demo). Both concepts are orthogonal: any Build Type can be applied to any Product Flavor.
Default Build Types include debug (debuggable=true, minification=false, signing=debug.keystore) and release (debuggable=false, minification=true, signing=production.keystore). The default Product Flavor is one, unnamed (effectively the main source set). Developers can add their own Build Types (e.g., “staging” with debuggable=true and minification=true) and any number of Product Flavors. Another difference is that Build Types cannot be grouped into dimensions, but Product Flavors can.
The key practical difference: defaultConfig in build.gradle applies to all Variants but can be overridden in productFlavors and buildTypes. A BuildConfigField added to a buildType is visible in all flavors of that type, while one added to a productFlavor is visible in all types of that flavor. If a field is defined in both, buildType takes priority (it is applied last in the chain).
| Characteristic | Build Type | Product Flavor |
|---|---|---|
| Purpose | How to build | What to build |
| Examples | debug, release, staging | free, paid, demo, enterprise |
| Default | debug + release | one (main) |
| Source Set | src/debug/, src/release/ | src/free/, src/paid/ |
| Dimensions | no | flavorDimensions |
| Application Order | after flavor, overrides | after defaultConfig |
| BuildConfigField | overrides flavor | overrides defaultConfig |
Build Variant configuration is done in the android block of the module-level build.gradle file. First, buildTypes are declared with their parameters, then flavorDimensions and productFlavors. Gradle automatically creates variants based on these declarations. Each variant inherits the module’s defaultConfig, overriding specified fields. The declaration order affects priority: buildTypes are applied after productFlavors.
To access a specific Build Variant in Gradle scripts, use android.applicationVariants (for app modules) or android.libraryVariants (for library modules). This is a collection that can be iterated over to modify each variant’s configuration at configuration runtime. For example, you can programmatically add buildConfigField for all variants containing the word “demo”.
Android Gradle Plugin 8.x added support for onVariants — a cleaner API for configuring variants via lambdas. The old API (variantOutput, variantFilter) is marked as deprecated. It is recommended to use onVariants together with onEach for library modules. Migrate from variantOutput to onVariants is a recommended step when upgrading AGP from 7.x to 8.x.
android {
buildTypes {
debug {
debuggable true
minification false
signingConfig signingConfigs.debug
}
release {
debuggable false
minification true
proguardFiles "proguard-rules.pro"
signingConfig signingConfigs.release
}
staging {
debuggable true
minification true
versionNameSuffix "-staging"
}
}
flavorDimensions "tier", "region"
productFlavors {
free { dimension "tier" }
paid { dimension "tier" }
us { dimension "region" }
eu { dimension "region" }
}
}
android.onVariants { variant ->
if (variant.name.contains("Demo")) {
variant.setEnabled(false)
}
}
Each Build Variant receives its own hierarchy of source sets — directories with source code, resources, and manifest. A source set is located at src/<variantName>/ (e.g., src/freeDebug/) and can contain java/, res/, AndroidManifest.xml, assets/. If a file exists in the variant’s source set, it overrides the file with the same name from the main source set (src/main/). For resources, merging occurs rather than replacement — the system merges resources from all active source sets, giving priority to variant-specific ones.
Source sets for a Build Variant are built in a chain: src/main/ → src/flavor/ → src/type/ → src/flavorType/. For example, for paidRelease, main is applied first, then paid, then release, then paidRelease. Each subsequent source set overrides the previous one. This means src/release/res/values/strings.xml will override the same strings from src/paid/, but src/paidRelease/res/ is even more prioritized.
Using source sets for variants is the recommended way to customize resources. Instead of checking BuildConfig.FLAVOR in code and branching logic, you can simply place different files in different source sets. For example, icons for free and paid versions go into src/free/res/ and src/paid/res/ respectively, and AndroidManifest with different permissions goes into src/free/AndroidManifest.xml and src/paid/AndroidManifest.xml. This is cleaner, faster (resources are compiled, not checked at runtime), and safer (you cannot accidentally include paid functionality in the free version due to a code bug).
In multi-module projects, each module (library) can have its own Build Variants. AGP automatically synchronizes variants: if the app module builds paidRelease, all dependent libraries are also built in their variants corresponding to paidRelease. A problem arises when a library does not have product flavors but the app module does — then the library is built once (release or debug depending on type).
For library modules, the Build Variant by default matches the app module’s Build Type, since libraries do not have product flavors. If a library needs to adapt to the app module’s flavor, the same flavorDimensions and productFlavors must be declared in the library. AGP matches flavors by exact name match. Gradle recommends synchronizing flavors through build configuration in the root project using subprojects or Convention Plugins.
Starting from AGP 8.1, libraries can publish multiple variants — publish all library variants to a maven repository simultaneously. This solves the problem when the app module uses a paid flavor but the library is only published for free. Multiple variants publishing (MVP) allows the dependent project to select the required variant automatically. To enable MVP, add publishing { multipleVariants { ... } } to the library’s build.gradle.
Sometimes it is necessary to disable some Build Variants — for example, if the mockRelease combination does not make sense (mock server should not go into production). Gradle provides variantFilter — a DSL block where you can check each variant’s properties and disable it via setIgnore(true). VariantFilter is applied at the configuration stage, before task creation, so a disabled variant does not generate assemble and install tasks.
Filtering is also useful for speeding up builds. If a project has 8 variants but a developer is working on only one, the remaining 7 variants still go through configuration. When using variantFilter, disabled variants do not create tasks, reducing configuration time by 30-50% for projects with 6+ flavor dimensions. In CI/CD, you can dynamically filter variants via command line parameters -PbuildOnly=paidRelease.
android {
variantFilter { variant ->
// Disable mock for release and demo for production
def names = variant.flavors*.name
def isMock = names.contains("mock")
def isDemo = names.contains("demo")
def isRelease = variant.buildType.name == "release"
if ((isMock && isRelease) || (isDemo && !isMock)) {
variant.setIgnore(true)
}
}
}
// Dynamic filtering via parameters
if (project.hasProperty("buildOnly")) {
def target = project.property("buildOnly")
android.variantFilter { variant ->
variant.setIgnore(variant.name != target)
}
}
Frequently Asked Questions
There is no limit, but Gradle creates the Cartesian product of all flavors and types. If you have 3 dimensions with 3 flavors each and 3 build types, you get 27 variants. Too many variants slow down configuration. It is recommended to have no more than 10–12 variants in one module.
flavorDimensions group Product Flavors into independent axes. For example, the “tier” dimension (free, paid) and “region” dimension (us, eu). Without dimensions, all flavors belong to one axis, and Gradle will only select one flavor from all (you cannot have free+us and paid+eu as separate variants).
In the productFlavor or buildType block, specify applicationId. For example, for the free version: free { applicationId “com.example.app.free” }. In the manifest, use ${applicationId} — Gradle will automatically substitute the value. This allows installing both variants on one device.
In iOS, the equivalent of Build Variants is the combination of Scheme + Configuration. Xcode Schemes are configured through Debug/Release configurations with different parameters. For multiple versions (free/paid), Build Configurations and Preprocessor Macros are used. On Android, the concept is more formalized and built into Gradle.
Yes, each variant can have a different APK size. Debug builds include debug information, SDK, and unsupported resources. Release builds with minification and resource shrinking produce the minimum size. Product Flavor also affects size: a free version without paid libraries will be smaller than a paid version by the size of those libraries.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also