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_URLin 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
debugImplementationfor diagnostics → powerful during dev, zero weight in releasebuildConfigFieldforBASE_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
manifestPlaceholdersper build type - Use
appLabelinAndroidManifest.xml - Optional: tweak icons in
src/debug/resvssrc/release/res
Diagnostics
- Guard tiny UI cues with
BuildConfig.DEBUG - Put dev libs under
debugImplementation - Keep release dependencies minimal
Environment config
- Define
buildConfigFieldforBASE_URLin each build type - Read
BuildConfig.BASE_URLin 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.