01 · Source icon
Drop an icon here
or click to browse · PNG / JPG / WebP · square recommended (≥512×512)

Badge text
Badge style
Position
Colors
Size
02 · Preview
Recipes · tap to load

Why variant icons matter (and how to ship them right)

If you've ever opened the wrong build of your app on your phone, debugged for fifteen minutes, and only then realized you were testing yesterday's release rather than the dev build with your latest changes, you already know why this tool exists. When the dev, staging, and production APKs all install with the same icon and same name, they look identical on the home screen and in the launcher tray. You're flying blind.

The fix is to make each build variant visually distinguishable. The standard approach is to override the launcher icon resource per Gradle build type: a different ic_launcher.png for debug than for release, with a visible label baked into the badged icon. This tool generates those variant icons.

Android density buckets, briefly

Android phones come in many screen densities. To keep icons sharp on every screen, your APK ships icon assets at multiple sizes, and the system picks the right one at install time. The density buckets and their mipmap-* directory names:

  • mdpi: 160 dpi baseline; 48×48 px icon.
  • hdpi: 240 dpi (1.5×); 72×72 px icon.
  • xhdpi: 320 dpi (2×); 96×96 px icon.
  • xxhdpi: 480 dpi (3×); 144×144 px icon.
  • xxxhdpi: 640 dpi (4×); 192×192 px icon.

The "Android density bundle" download from this tool gives you all five at once, in the correct directory structure. Drop the unzipped folders straight into app/src/<variant>/res/ and Gradle picks them up automatically.

How to wire it into your Gradle build

In your app/build.gradle, define your build types:

android {
    buildTypes {
        debug { applicationIdSuffix ".debug" }
        release { /* prod */ }
        staging {
            initWith debug
            applicationIdSuffix ".staging"
        }
    }
}

Then create matching source set folders and drop the badged icon bundle inside each:

app/src/debug/res/
    mipmap-mdpi/ic_launcher.png        ← from this tool's bundle
    mipmap-hdpi/ic_launcher.png
    mipmap-xhdpi/ic_launcher.png
    mipmap-xxhdpi/ic_launcher.png
    mipmap-xxxhdpi/ic_launcher.png

app/src/staging/res/
    mipmap-mdpi/ic_launcher.png        ← different bundle
    ...

Gradle merges the variant-specific resources over the main source set, so your production icon (in app/src/main/res/) only ships in release builds. The debug build replaces it with the badged DEV version, the staging build replaces it with the badged STAGING version, etc. Combined with applicationIdSuffix, you can have all three variants installed on the same device side by side, each with its own icon. No more confusion.

Adaptive icons: a known caveat

On Android 8 (API 26) and later, apps can ship adaptive icons with separate foreground and background layers, and the system applies a launcher-defined mask (circle, squircle, rounded square, depending on the OEM). The tool above operates on the legacy square ic_launcher.png, which adaptive-icon launchers ignore in favor of the adaptive layers.

That means: if your app ships adaptive icons, badging the legacy square here will only show up on Android 7.x and below, plus on launchers that fall back to the legacy icon. For modern devices, you'd need to badge the foreground layer separately, which is a more complex workflow because the foreground is meant to be inset within a safe zone (the system can crop the outer 18% on round masks). A practical-enough workaround for variant marking: use this tool to badge the legacy icon and override the adaptive icon's foreground in the variant source set with a separately-edited foreground XML. For most internal teams shipping internal-only debug builds, the legacy badging is enough; the launcher behavior still surfaces it on enough devices to prevent build confusion.

Badge design that works at 48 pixels

Keep the text short, three to six characters. DEV, STAGING, QA all read fine; development build v2 is unreadable at mdpi (48×48 px) and only marginally legible at xxxhdpi. Use all caps. Cap height fills more pixels per glyph than lowercase, which matters more than you'd think at the smaller densities. Lowercase descenders just turn into mush.

Pick high contrast against the icon. A saturated badge color (red, blue, orange) with white text works against most icons; if your icon is itself very light or white, a dark badge stands out more than a colored one would. Position matters too. If your icon has a logo dead-center, a corner pill is safer than a diagonal ribbon that crosses the logo. If your icon is abstract or asymmetric, a ribbon actually adds character. You can almost treat the badge as part of the design.

Pick a styling convention per environment and stick to it. Always-red for dev, always-orange for staging, blue for beta. The point isn't the specific colors; it's the consistency. Your eye trains over weeks of seeing the dev icon in the same color and starts recognizing the build at a glance. If every variant gets a different look in every release, you've defeated the entire purpose.

A loose convention has emerged in Android teams over the years, and your colleagues will likely recognize it without explanation: red for DEV (the "unfinished, don't trust it" color), orange for STAGING (between dev and prod, candidate builds), blue for BETA (public preview, mostly stable), green for QA (passed dev, awaiting validation), purple for INTERNAL (employee-only, dogfooding), and dark gray or black for NIGHTLY (automated overnight builds, freshness varies). None of this is official. It's just common enough that you can default to it without explanation.

One more thing: don't ship badged icons to production

It's happened. Someone uploads a debug-badged APK to the Play Store. Reviewers reject it. Or, worse, it ships and users see "DEV" on their app icon for two weeks until someone in marketing notices and files an urgent ticket. The defense is structural, not procedural: keep your variant icon source files only inside app/src/debug/res/ or app/src/staging/res/, never in main. The Gradle build automatically picks the unbadged production icon for release builds because that's the only icon in main/res. The variant directories shouldn't even exist for the production source set. If you ever find yourself adding badged icons to main "temporarily," stop. That's how prod-with-DEV-icons happens.

The adaptive-icon picture in 2026

The earlier section glossed over adaptive icons because the workflow for the common case is straightforward. The full picture is more nuanced and worth pinning down, because the adaptive-icon system has evolved in three significant ways since API 26 (2017), and advice from older blog posts is often wrong by now.

First, since Android 13 (API 33), a third layer (the monochrome drawable) exists alongside foreground and background. When the user enables themed icons on the home screen (Settings → Wallpaper & style → Themed icons), the launcher discards your foreground and background entirely and tints the monochrome silhouette to match the wallpaper. If your app doesn't ship a monochrome layer, the launcher renders a fallback that looks worse than your normal icon, so users with themed icons see your app degraded. Variant badging on the monochrome layer is technically possible but rarely worth the effort; the audience that runs themed icons is small and self-selecting, and they're unlikely to be testing dev builds anyway.

Second, the safe zone has gotten meaner. The original API 26 spec was that the outer 33% of the foreground might be cropped by the launcher mask, leaving a central 66×66 dp safe zone within the 108×108 dp canvas. Pixel Launcher and Samsung One UI both respect this. Some third-party launchers (Nova, Niagara) crop more aggressively in certain modes. A badge placed precisely at the corner of the foreground often gets clipped on those, which means your "DEV" pill in the top-right looks fine on stock Pixel and disappears on a custom launcher. If you support a wide range of test devices, keep badges 15% inside the canvas edge rather than at it.

Third, the launcher animation cost matters more in 2026 than it did. Modern launchers animate icons on tap (Android 14+ haptic-coupled scale), on drag (Material 3 elastic pull), and on icon pack swap. The launcher caches the rendered icon, but the cache is invalidated whenever the icon file changes. Frequent icon swaps (a CI-generated badge that includes the build number) cost more in launcher redraw than you'd think, and produce a visible flicker on the home screen when the user opens the launcher after an install. Use stable per-environment badges (DEV, STAGING) rather than per-build badges (DEV-build-2491) unless you have a strong reason.

The legacy-only case, expanded

A surprising number of Android shops still maintain projects targeting API 24 or 25 because of fleet management contracts, kiosk hardware, or in-vehicle infotainment builds. On those targets there's no adaptive icon system at all, and this tool's output drops directly into mipmap-mdpi/ic_launcher.png through mipmap-xxxhdpi/ic_launcher.png with no further work. The five density buckets are still the standard. The only thing to watch is round icons: the ic_launcher_round.png resource shipped alongside the square one was introduced in API 25 for the brief window of round-mask launchers between API 25 and API 26's adaptive system. If your project predates that window, you only need square icons. If your project targets API 25 specifically, ship both, and badge both, or the round mask will surface a different unbadged icon on some OEMs.

By Belchior · Last updated · May 2026

Copied