Android Duplicate Resources: mergeDebugResources Fails
Rename or consolidate the twins the merge log names: duplicate resources mean two modules claim one name, and guessing ships bugs..
20+ years shipping production backend systems. Drawn from code that ran under real load.
- ✓An Android project with at least one flavor or library module
- ✓Comfort editing XML resources and manifests
- ✓Ability to run Gradle tasks from Android Studio or terminal
- Duplicate resources mean two sources define the same type, name, and qualifiers — the merge log names both files
- Decide one canonical owner: consolidate shared values into a base module, rename the rest
- Give every library a resourcePrefix so generic names like ic_close can't collide
- Settle manifest clashes with narrowly scoped tools:replace, not blanket overrides
- You'll shrink future collisions with resConfigs keeping only the densities and locales you ship
Imagine two roommates each label a jar 'sugar' — one holds sugar, the other holds salt. A recipe calling for 'sugar' grabs whichever jar is closer, and dinner is ruined. Android's resource merge faces the same problem: two files named the same thing, one winner, no way to know which is right — so the build stops and asks you. The lasting fix is labeling jars by owner ('Asha's sugar'), keeping one shared pantry for common items (a base module), and throwing out sizes you never use (resConfigs).
app:mergeDebugResources fails, names two files you never wrote together, and blocks the whole build. Duplicate resources are Android's merge-time collision: two modules claim the same resource name (same type, same qualifiers), and the build refuses to guess which one you meant. Main versus flavor, app versus library, library versus library — the combinations multiply with every module you add.
The error message is genuinely helpful — it prints both paths — yet teams still lose hours. They rename one file and uncover the next collision, or 'fix' it with a qualifier hack that breaks a different flavor. Library authors who ship btn_ok and ic_close guarantee every host app collides eventually. And the silent variant is worse: same-name resources that merge without failing but resolve to the wrong value, shipping wrong strings to users with a green build.
This article ends the whack-a-mole. You'll learn to read the merge error precisely, consolidate or rename with resPrefix discipline, settle manifest clashes with scoped tools:replace, trim the merge surface with resConfigs, and gate it all with lint. Six sections, one policy, zero duplicate builds.
Reading the Merge Error: Finding Both Claimants
The merge error names the crime precisely: resource type, name, and the two files claiming it. Same string in main and a flavor, same drawable in app and library, same color in two libraries — the merger compares type + name + qualifier bucket, and any double-claim in one bucket fails the task. Read both paths before touching anything: the fix depends on which pair collided. Main-vs-flavor is usually an intentional override gone stale; app-vs-library is usually a generic library name; library-vs-library means at least one vendor chose poorly.
Decide canonical ownership first. Shared values (brand colors, common strings) belong in exactly one place — main source set or a shared :ui-core module — with all copies deleted. True overrides (flavor app_name, per-brand endpoints as strings) stay duplicated deliberately, one per source set, with a comment stating intent so the next reader doesn't 'dedupe' them. Everything else gets renamed: the app's private copy keeps the clear name, the library adopts its prefix.
Verify with the task itself, not a full build. ./gradlew :app:mergeDebugResources --info reruns just the merge and prints remaining duplicates, so you can fix iteratively in seconds per cycle. When it passes, assemble the affected flavors (not just debug) because qualifiers differ per variant — a drawable clash hiding in drawable-fr may not exist in default. One merge run per flavor closes the loop completely.
Killing Collisions With resourcePrefix Discipline
Generic names are collision guarantees. Every library shipping btn_ok, ic_close, or app_name will eventually meet a host app with the same names, and the merge makes someone lose. The resPrefix convention fixes this structurally: each module owns a short prefix (pay_, brandkit_, onboarding_), and every resource it declares starts with it. Collisions become near-impossible because namespaces no longer overlap by accident.
Apply it on both sides of the boundary. Library authors set resourcePrefix in the library's build.gradle, which makes Gradle warn on unprefixed names — turn that warning into a review-blocking rule. App teams prefix feature-module resources the same way (:checkout uses checkout_, :search uses search_), so internal modules stop colliding with each other as the app grows past ten modules. The renames are mechanical (Android Studio refactor handles XML + code references together) and safe to batch.
Expect mild grumbling about verbosity — pay_btn_ok is longer than btn_ok — and answer with incident math. One silent wrong-icon shipment costs more than a thousand extra characters. After the rename pass, add the prefix check to CI lint so new resources comply from birth. Prefix discipline is the only fix in this article that prevents collisions instead of resolving them, which makes it the highest-leverage one.
Manifest Clashes and Scoped tools:replace
Manifests merge too, with their own conflicts: two libraries declaring the application label, an activity with the same name but different config, overlapping permissions. The merger report (app/build/outputs/logs/manifest-merger-*-report.txt) shows every contributor and the winner per element — read it before editing, because the fix must target the exact contested attribute. Blanket tools:replace on the whole application tag overrides everything including entries you never meant to touch.
Scope every override to the attribute you intend to win. tools:replace='android:label' takes only the label; tools:remove drops a specific inherited permission; tools:node='merge' keeps default behavior explicit. After each change, re-read the report: confirm your value won and confirm the library's activities, services, and permissions still merged. The classic follow-up incident is a SecurityException weeks later from a permission your broad replace silently discarded.
Library authors can reduce these fights by claiming less. Don't set application labels or icons in SDK manifests unless the SDK truly owns the launcher surface; don't request broad permissions a minority of hosts need. Each unclaimed attribute is a merge conflict that never happens. When reviewing SDK updates, diff the merged manifest report — new claims appear there before they appear as runtime surprises.
Terminal Triage: grep, Merge Tasks, and Clean Rebuilds
The terminal gives you the fastest loop for merge failures. The merge task with --info prints the duplicate pair with full paths — grep for 'duplicate' to skip the noise. A repo-wide grep for the contested name (with escaped quotes, scoped to res dirs) reveals every claimant including ones the failing variant didn't mention; fix them all in one pass instead of rediscovering them flavor by flavor. Re-run the merge task per affected variant, since qualifiers make each variant a different merge.
Clean builds resolve the phantom class of failures: renamed resources whose stale outputs linger in incremental state. When clean passes but incremental fails, suspect the cache before your fix — verify Gradle/AGP versions, check that no two source sets map the same res directory, and confirm no build step generates resources with colliding names. Don't normalize clean builds as the workflow; fix the staleness so incremental stays trustworthy.
Script the audit for reuse. A one-liner that greps a resource name across modules belongs in your runbook; a lint baseline tracks the warnings you've accepted. The goal is leverage: each collision you fix should make the next one faster, through prefixes, consolidated modules, and checks that run before humans get involved. Manual grep is triage; automation is the cure.
resConfigs, Lint Gates, and Proof Per Variant
resConfigs shrinks the merge surface by declaring what you actually ship. Every library bundles densities, locales, and sometimes ABIs you don't need; each bundled bucket is a collision candidate and APK weight. Declaring resConfigs('en', 'fr', 'xxhdpi') keeps exactly those and strips the rest at build time. Conflicts in stripped buckets vanish, the APK gets smaller, and resource processing speeds up — one declaration, three wins.
Pair it with lint's resource checks for ongoing hygiene. Android lint flags duplicate resources, unused resources, and translation drift (MissingTranslation/ExtraTranslation) across modules; run lintDebug in CI and fail on new warnings. The lint report names modules and files, routing each fix to the owning team without a central resource czar. Baseline existing warnings once, then hold the line — every new warning is a future incident volunteering early.
Close the loop with APK Analyzer and screenshot tests. Analyzer's resource table proves each name resolves to the intended file per variant; screenshot tests catch the silent wrong-value merges that compile clean. One launch-screen shot per flavor is cheap insurance against the exact incident class that green builds hide. Together — resConfigs, lint gates, analyzer spot-checks, screenshots — duplicates stop being a category and become a historical footnote. Your merge stays green because the system prevents collisions instead of you hunting them each release.
Keeping a Multi-Module App Collision-Free
Codify the policy so twenty modules can't drift. Publish a one-page resource guide: prefixes per module, shared values live in :ui-core, flavor overrides documented with intent comments, resConfigs declared in the app module, lint gates blocking new warnings. Link it from the module-creation checklist so new modules start compliant instead of being fixed later.
Review for it mechanically. New resources without the module prefix, new strings copied instead of shared, new manifest claims without justification — each gets a standard comment pointing at the guide. After a month the pattern becomes habit and review load drops; after a quarter, run the numbers (duplicate incidents, APK size, lint warnings) and share the trend. Visible improvement sustains conventions better than any wiki mandate.
Revisit on every SDK integration, because vendors reset the clock with their own naming. Before adding a library, grep its AAR for generic top-level names and check its manifest claims in the merger report. Rename on your side preemptively or isolate the SDK behind a wrapper module with its own prefix. Ten minutes of pre-integration audit beats a release-week collision every time. Record each audit's findings beside the dependency declaration so the next upgrade starts from evidence, not rediscovery.
The Wrong Splash Shipped Green: a Silent SDK Resource Override
- Green builds can ship wrong resources — silent merges need screenshot tests, not just compile gates.
- Generic library names (splash_logo, ic_close) collide with every host eventually; prefix from day one.
- A lint gate on duplicate warnings converts the next collision from a release incident into a compile error.
| File | Command / Code | Purpose |
|---|---|---|
| flavorPaid | <!-- BEFORE: main/res/values/strings.xml --> | Reading the Merge Error |
| library | <!-- library/build.gradle: enforce prefixed names --> | Killing Collisions With resourcePrefix Discipline |
| app | <!-- app/src/main/AndroidManifest.xml: scoped override --> | Manifest Clashes and Scoped tools |
| terminal | ./gradlew :app:mergeDebugResources --info 2>&1 | grep -A 3 -i 'duplicate' | Terminal Triage |
Key takeaways
Common mistakes to avoid
5 patternsGeneric resource names in library modules (btn_ok, ic_close)
Copy-pasting the same strings and colors into every flavor
Assuming the build picks the 'right' duplicate
Using tools:replace as a blanket fix for manifest clashes
Shipping every density and locale a library bundles
Interview Questions on This Topic
What causes app:mergeDebugResources duplicate resource errors?
Frequently Asked Questions
20+ years shipping production backend systems. Drawn from code that ran under real load.
That's Android. Mark it forged?
5 min read · try the examples if you haven't