DEV Community

Cover image for Android build types, product flavors, and variants from zero to production
Mark Kazakov
Mark Kazakov

Posted on • Originally published at promobile.dev

Android build types, product flavors, and variants from zero to production

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"
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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\""
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

LeakCanary only in debug.
Timber only in demo builds.

This keeps production builds clean.

Top comments (0)