Android Development
Product Flavors
Dependency Management
Build Configuration
Gradle

How to define different dependencies for different product flavors

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In Android builds, product flavors let you ship multiple variants of the same app from one codebase. If each flavor needs a different SDK or library, Gradle supports flavor-specific dependency configurations such as freeImplementation and paidImplementation.

Define the flavors first

Start by declaring a flavor dimension and the flavors that belong to it. In the Android Gradle plugin, dependencies become flavor-aware only after the flavors exist.

kotlin
1android {
2    namespace = "com.example.app"
3    compileSdk = 35
4
5    flavorDimensions += "tier"
6
7    productFlavors {
8        create("free") {
9            dimension = "tier"
10            applicationIdSuffix = ".free"
11            versionNameSuffix = "-free"
12        }
13
14        create("paid") {
15            dimension = "tier"
16            applicationIdSuffix = ".paid"
17            versionNameSuffix = "-paid"
18        }
19    }
20}

This creates two variants in the tier dimension: free and paid.

Add flavor-specific dependencies

Once the flavors exist, Gradle exposes matching configurations. Shared libraries still go in implementation, while flavor-only libraries go in the generated flavor configurations:

kotlin
1dependencies {
2    implementation("androidx.core:core-ktx:1.13.1")
3    implementation("androidx.appcompat:appcompat:1.7.0")
4
5    freeImplementation("com.google.android.gms:play-services-ads:23.3.0")
6    paidImplementation("com.example.billing:premium-sdk:2.1.0")
7
8    freeDebugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
9}

That means:

  • every variant gets the shared implementation dependencies,
  • only the free flavor gets the ads SDK,
  • only the paid flavor gets the premium SDK,
  • and only the freeDebug variant gets LeakCanary in this example.

How Gradle resolves these configurations

Gradle combines build type and flavor information into a variant. For example, freeRelease inherits:

  • 'implementation'
  • 'freeImplementation'
  • 'releaseImplementation'

This layered model is why flavor-specific dependency declarations are so useful. You do not need duplicate modules or manual if statements in the build script.

Combine flavor-specific code and resources

Dependencies are only part of the story. Product flavors are usually paired with matching source sets:

text
1src/free/java/...
2src/free/res/...
3src/paid/java/...
4src/paid/res/...

If the free flavor depends on an ads SDK, put the ad-specific classes under src/free. That keeps the paid build from even compiling code that references the ad library.

For example, a free-only helper class might live in:

text
src/free/java/com/example/app/AdsManager.kt

while the paid flavor provides a no-op replacement in:

text
src/paid/java/com/example/app/AdsManager.kt

Both variants compile cleanly, but each uses the implementation appropriate to its dependency set.

Common Pitfalls

The most common issue is using the old compile style or placing a flavor-only library under plain implementation. If you do that, every flavor sees the dependency, which defeats the point of variant-specific builds.

Another problem is forgetting to assign a dimension to every flavor. Gradle requires all product flavors to belong to a declared flavor dimension.

Be careful with code references across source sets. If main/ imports a class from a dependency that only exists in freeImplementation, the paid variant will fail to compile.

Finally, keep version alignment in mind. If two flavors depend on incompatible transitive versions of the same library, you may need explicit dependency constraints or exclusions to keep the build stable.

It is also worth checking the generated Gradle tasks and variant graph in Android Studio. When a dependency only fails in paidRelease or freeDebug, the problem is often a variant-specific configuration rather than the dependency declaration itself.

For teams maintaining many variants, naming discipline matters. Clear flavor names and consistent source-set structure make dependency issues much easier to trace when a build starts failing months later.

That upfront consistency pays off every time a new dependency is introduced.

Summary

  • Define product flavors first, then use the generated flavor-specific configurations.
  • Put shared libraries in implementation and flavor-only libraries in freeImplementation, paidImplementation, and similar configurations.
  • Remember that Gradle resolves dependencies per variant, not just per flavor.
  • Use flavor-specific source sets alongside flavor-specific dependencies so code stays separated cleanly.
  • Check dimensions, source-set references, and transitive dependency conflicts when a flavored build fails.

Course illustration
Course illustration

All Rights Reserved.