A team's AGP build has grown to dozens of variants and CI is slow. How do you reason about and control the variant explosion?
answer
- matrix is multiplicative
- prune with beforeVariants / variantFilter
- fewer dimensions, not more
- CI builds only needed variants, not assemble-all
- build cache + config cache + modularization
basics
~10 sVariants multiply (flavors-per-dimension × build types), so they explode fast. Prune impossible combinations with beforeVariants/variantFilter, reduce dimensions, and have CI build only the variants it actually needs.
solid answer
~50 sBecause the variant matrix is a **Cartesian product**, every new flavor or dimension multiplies the total — e.g. 4 flavors × 2 dimensions × 3 build types can hit 48 variants, each with its own compile/merge/assemble chain. The control levers: (1) **prune** combinations that make no sense using `androidComponents { beforeVariants { it.enable = false } }` (legacy: `variantFilter`); (2) **minimize dimensions** — collapse concerns that don't truly need to combine; (3) **scope CI** to build only required variants (e.g. only `prodRelease` + `debug` smoke variants) rather than `assemble` (which builds all); (4) lean on Gradle's **build cache and configuration cache** so unchanged variants aren't rebuilt; (5) consider **modularization** so white-label differences live in modules rather than flavors. The first instinct should be: does this combination need to exist? Most explosions come from accidentally combinatorial dimensions.
code
kotlin · 9 linesandroidComponents {
beforeVariants { variant ->
val flavors = variant.productFlavors.map { it.second }
// Never build: free tier in release, or mock backend in prod
val impossible = ("free" in flavors && variant.buildType == "release") ||
("mock" in flavors && "prod" in flavors)
if (impossible) variant.enable = false
}
}go deeper
Recognize that variants multiply and that's why builds get slow.
Know variantFilter/beforeVariants pruning and scoping CI to specific assemble tasks.
Reason about dimension design, lazy Variant API, build/configuration cache, and when to modularize vs add flavors.
Set org-wide variant budgets, remote build-cache strategy, CI variant matrices per pipeline stage, and white-label modularization standards.
## Why it explodes Variants are **multiplicative**: `∏(flavors per dimension) × (#build types)`. Add one dimension with 3 flavors and you triple the matrix. Each variant carries a full task chain (compile, merge resources, process manifest, assemble, bundle, unit-test, instrumented-test), so CB build time and configuration time both grow roughly linearly with variant count. ## Levers, in priority order **1. Question the combination (design).** The cheapest variant is the one you never create. If `free` should never ship as `release`, or `mock` env makes no sense in `prod`, those cells shouldn't exist. **2. Prune with the Variant API.** ```kotlin androidComponents { beforeVariants { v -> val isFree = v.productFlavors.any { it.second == "free" } if (isFree && v.buildType == "release") v.enable = false } } ``` This runs *before* variants are realized, so disabled variants cost nothing. (Older AGP: `android.variantFilter { if (...) ignore = true }`.) **3. Reduce dimensions.** Two dimensions of 2 = 4; one dimension of 3 = 3. Collapse concerns that don't independently vary. Sometimes a *build type* (initWith) or a *gradle property* is a better fit than a whole flavor dimension. **4. Scope what CI builds.** `./gradlew assemble` builds *every* variant. Instead target the few you ship/test: `assembleProdRelease`, plus a single debug smoke variant. PR builds rarely need all variants. **5. Caching.** Enable the **build cache** (`org.gradle.caching=true`) and **configuration cache** so unchanged variants reuse outputs and configuration time is amortized. Use a remote build cache in CI so variants built once are reused across agents. **6. Modularize.** For heavy white-label scenarios, push per-client code into modules/feature modules selected by dependency wiring instead of multiplying flavors. This trades flavor explosion for module composition. ## How to measure `./gradlew :app:tasks` and a build scan reveal per-variant task time; the configuration phase shows variant realization cost. If configuration time is the bottleneck, ensure you're on the lazy Variant API and configuration cache. ## The interview takeaway Lead with *design* (do these combinations need to exist?), then *prune*, then *cache/scope*. Throwing more CI hardware at a 48-variant matrix is the last resort. This is an overview-level concern; deep AGP variant-API and manifest specifics live in the Android docs.
- Why is pruning with `beforeVariants` cheaper than filtering later?It runs before the variant is realized, so AGP never creates its tasks/configurations or pays configuration cost — disabled variants are essentially free, whereas post-realization filtering still pays to build the variant model.
- When is modularization a better fix than flavors?When per-client/per-edition differences are large amounts of code; modules selected by dependency wiring avoid multiplying the whole variant matrix and keep build graphs cacheable per module.
- How does the configuration cache help with many variants?It serializes the configured task graph so the (expensive) configuration phase, which scales with variant count, is skipped on subsequent builds when inputs are unchanged.
It's like a restaurant menu where every option combination becomes a dish you must prep: trim impossible combos and pre-cook (cache) the rest so the kitchen doesn't drown.
saying these in an interview costs you the question
- Jumping straight to more CI hardware instead of pruning/design
- Building all variants in every PR via `assemble`
- Adding flavor dimensions casually without considering the multiplicative cost