Back to Articles

Android Build Types in CI/CD: Make Variants Obvious, Safe, and Environment-Aware

Learn how to give debug and release builds distinct labels, keep diagnostics in debug with debugImplementation, and set BASE_URL per build type using buildConfigField

Companion to the video:

Previous Article: Android Build Types Explained: Debug vs Release, Suffixes & BuildConfig Beginner’s Guide


Why this matters

I used to think build types were just “debug = easy, release = strict.” Now the important part is: your Android builds are artifacts in a CI/CD pipeline. They need to be easy to identify, safe to test, and point to the right backend without manual tweaks. In real projects, a little polish here saves a lot of human time later.

This guide shows three moves:

1 Make debug vs release obvious on the device
2 Keep diagnostics in debug only e.g., LeakCanary
3 Set BASE_URL per build type for staging vs production

What I’ve noticed is these are small changes with outsized payoff during continuous testing and distribution.

1 Problem: both apps look the same on the device

This is where most people get stuck: you install debug and release together to compare behavior, but both show the same name and icon label. You tap the wrong one, rerun tests, and lose minutes every time.

A simple way to think about it is: give each build type its own label.

Term check quick: Manifest placeholder → a value you inject into AndroidManifest.xml from Gradle.

2 Clean fix: manifest placeholders for app labels

Step 1: define placeholders per build type

In your app build.gradle Groovy shown, set a unique label value for each build type.

android 
  buildTypes 
    debug 
      manifestPlaceholders = [ appLabel: "Variant Demo DBG" ]
    
    release 
      manifestPlaceholders = [ appLabel: "Variant Demo" ]
    
  

If you’re a beginner, this is the part that ties Gradle values to your manifest at build time.

Step 2: use the placeholder in AndroidManifest.xml

<application
    android:label="appLabel"
    ... >
</application>

Result: install both variants, and they’re clearly different in the launcher and Settings. The mistake people make is leaving @string/app_name hardcoded, which makes side-by-side testing painful.

3 Debug-only diagnostics: keep release clean

In my experience, tools like LeakCanary are priceless during development but add overhead if they leak into production builds. What usually happens is people add the library with implementation and forget it ships with release too.

Term check quick: debugImplementation → a Gradle configuration that includes a dependency only in the debug build type.

3.1 Show a tiny debug indicator optional but helpful

A lightweight pattern is to expose a small “debug chip” or button only when BuildConfig.DEBUG is true.

if BuildConfig.DEBUG 
    // Example: show a small badge or a developer menu entry
    showDebugBadge
  • Why: immediate visual confirmation you’re in the debug build.
  • Tip: keep it small and non-blocking.

3.2 Add debug-only dependencies

dependencies 
  debugImplementation "com.squareup.leakcanary:leakcanary-android:<version>"
  // No LeakCanary in release

On paper this looks simple, but in bigger apps it’s the habit that keeps your release APK lean and avoids accidental diagnostics in production.

4 Build-type BASE_URL: staging vs production

A simple way to think about it is: code stays the same, only the build type decides which backend you talk to.

Term check quick: buildConfigField → exposes a constant into BuildConfig for the current build type.

android 
  buildTypes 
    debug 
      buildConfigField "String", "BASE_URL", ""https://staging.api.example.com""
    
    release 
      buildConfigField "String", "BASE_URL", ""https://api.example.com""
    
  

Use it in code:

val baseUrl = BuildConfig.BASE_URL
  • Practical tip: log the BASE_URL in debug or surface it in your debug screen so testers always know which environment the app is using.
  • CI/CD fit: your debug artifacts go to internal testers staging, while release artifacts are wired for production — no code edits between them.

5 Why if BuildConfig.DEBUG doesn’t scale

According to me, if BuildConfig.DEBUG gates are fine to start, but they don’t age well. As teams grow:

  • Debug-only logic spreads across files
  • You risk leaving debug paths wired into production code
  • Config lives in code, not in structure

In real projects, you want separation rather than sprinkling flags everywhere.

6 The next step: source sets clean separation

Source sets let you place debug-only code/resources in src/debug/... and release-specific ones in src/release/.... A simple way to think about it is: two drawers—put dev tools in the debug drawer, and customer-facing code in the release drawer.

Example:

app/
  src/
    debug/
      java/... Debug-only classes, stubs, dev menus
      res/...  Debug icons, strings
    release/
      java/... Prod implementations
      res/...  Prod icons, strings
  • Benefit: fewer conditionals, cleaner diff, easier reviews.
  • CI/CD: each artifact naturally picks up the right code and resources for its environment.

Quick recap

What I’ve noticed is these three patterns pay back immediately during CI/CD:

  • Distinct labels per build type → install both, no confusion
  • debugImplementation for diagnostics → powerful during dev, zero weight in release
  • buildConfigField for BASE_URL → the right environment without hand edits

And when your app grows, shift from flags to source sets. If you’re a beginner, this is the part that feels like overkill. But once multiple teams and modules enter the picture, the structure keeps you safe.

Practical checklists

Labeling builds

  • Add manifestPlaceholders per build type
  • Use appLabel in AndroidManifest.xml
  • Optional: tweak icons in src/debug/res vs src/release/res

Diagnostics

  • Guard tiny UI cues with BuildConfig.DEBUG
  • Put dev libs under debugImplementation
  • Keep release dependencies minimal

Environment config

  • Define buildConfigField for BASE_URL in each build type
  • Read BuildConfig.BASE_URL in your networking layer
  • Surface the current URL in a debug-only screen

Final thought

For me, the goal is boring builds: obvious to identify, safe to ship, and predictable in pipelines. Move those knobs into the build system, not scattered if checks. Your future self and your testers will thank you.