Home › Mobile › Android Duplicate Resources: mergeDebugResources Fails
Intermediate 5 min · September 23, 2026

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..

N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Drawn from code that ran under real load.

Follow
✓ Production
production tested
September 27, 2026
last updated
2,085
articles · all by Naren
Before you start⏱ 12 min
  • ✓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
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • 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
✦ Definition~90s read
What is Android app?

Android builds merge resources from every source set — main, build-type and flavor overlays, library modules, and AAR dependencies — into a single table the app compiles against. The merge key is type + name + configuration qualifiers: string/app_name in default qualifiers is one slot, and drawable-hdpi/icon is another.

★
Imagine two roommates each label a jar 'sugar' — one holds sugar, the other holds salt.

When two sources claim the same slot, the task app:mergeDebugResources (and its per-variant siblings) must produce one value. For genuine double-claims it fails loudly with 'Duplicate resources' and both paths; for some app-vs-library overlaps it resolves by priority and ships the winner silently.

Priority rules decide silent cases: flavor and build-type overlays beat main, and app sources beat libraries. tools:replace and tools:remove markers in manifests override per element. Densities, locales, and other qualifiers partition the table — drawable/icon in hdpi and xhdpi are complements, not conflicts.

The merged-manifest report and the merge task's --info output expose exactly which file won each slot, which is why reading build outputs beats guessing.

The structural fixes follow the model. Unique names (per-module prefixes via resourcePrefix) prevent double-claims; consolidation (shared :ui-core module) removes them; deliberate flavor overrides document the intended duplication; resConfigs shrinks the table by stripping unshipped buckets; scoped tools:replace settles manifest contests without collateral.

Lint's duplicate/unused/translation checks plus APK Analyzer plus per-flavor screenshots verify the outcome. Understand slots, priority, and qualifiers, and every merge failure becomes a ten-minute routing decision.

Plain-English First

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.

flavorPaid/res/values/strings.xmlXML
1
2
3
4
5
6
7
8
9
10
11
12
<!-- BEFORE: main/res/values/strings.xml -->
<!-- <string name="app_name">ShopApp</string> -->

<!-- BEFORE: flavorPaid/res/values/strings.xml -->
<!-- <string name="app_name">ShopApp Pro</string> -->  <!-- DUPLICATE -->

<!-- AFTER: keep the override ONLY where behavior differs, -->
<!-- and move everything shared to main. Merge picks flavor over main -->
<!-- for the same name — intentional here, documented in review. -->
<resources>
    <string name="app_name">ShopApp Pro</string>
</resources>
📊 Production Insight
Iterating on the merge task alone (not full assemble) turns a ten-minute guess cycle into a thirty-second confirm cycle.
🎯 Key Takeaway
The error lists both files. Consolidate shared values, keep deliberate flavor overrides, rename the rest.

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.

library/res/values/strings.xmlXML
1
2
3
4
5
6
7
8
9
10
<!-- library/build.gradle: enforce prefixed names -->
<!-- android { resourcePrefix 'pay_' } -->
<!-- Renamed: btn_ok -> pay_btn_ok, ic_close -> pay_ic_close -->
<resources>
    <string name="pay_title">Pay now</string>
    <string name="pay_cancel">Cancel</string>
</resources>

<!-- app code references the prefixed names; collisions gone -->
<!-- findViewById / getString(R.string.pay_title) -->
📊 Production Insight
SDK teams that adopt prefixes report collision support tickets dropping to zero — host apps simply stop overlapping.
🎯 Key Takeaway
Prefix every module's resources; enforce with Gradle's resourcePrefix and a CI lint gate.

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.

app/src/main/AndroidManifest.xmlXML
1
2
3
4
5
6
7
8
9
<!-- app/src/main/AndroidManifest.xml: scoped override -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <application
        android:label="@string/app_name"
        tools:replace="android:label">
        <!-- Only label is overridden; library components stay merged. -->
    </application>
</manifest>
📊 Production Insight
Overbroad tools:replace discarding a library permission is a top cause of delayed SecurityException crashes post-integration.
🎯 Key Takeaway
Override only the contested attribute, verify survivors in the merger report, and claim less in library manifests.

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.

terminalBASH
1
2
3
4
5
6
7
8
# Show the exact duplicate pair:
./gradlew :app:mergeDebugResources --info 2>&1 | grep -A 3 -i 'duplicate'

# Audit every definition of a name across modules:
grep -rn 'name="app_name"' app/src library/src --include='*.xml'

# After fixing: clean merge per variant
./gradlew clean :app:mergeDebugResources :app:mergePaidReleaseResources
📊 Production Insight
Variant-scoped merge reruns catch qualifier-specific duplicates that a single debug build never exercises.
🎯 Key Takeaway
Grep all claimants, fix per variant, and automate the audit so each collision speeds up the next.

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.

⚠ Never Delete Overrides Blindly
Don't 'fix' duplicates by deleting the flavor's override and hardcoding one value. You'll ship the wrong name, icon, or endpoint to every other flavor — a green build with wrong behavior in each variant you didn't check.
📊 Production Insight
Screenshot-per-flavor tests are the only gate that catches silent wrong-value merges — add them to the flavors that carry brand risk.
🎯 Key Takeaway
Strip unshipped configs, fail CI on new lint warnings, and verify winners with analyzer plus screenshots.

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.

📊 Production Insight
Pre-integration AAR audits take ten minutes and prevent the release-week collisions that cost days.
🎯 Key Takeaway
Write the one-page policy, review prefixes mechanically, and audit every new SDK before it merges.
● Production incidentPOST-MORTEMseverity: high

The Wrong Splash Shipped Green: a Silent SDK Resource Override

Symptom
After integrating a white-label SDK, the banking app's launch screen showed the vendor's generic logo instead of the bank's brand. The build was green, Crashlytics was quiet, and the wrong splash reached production for two days before a customer screenshot reached support.
Assumption
QA assumed the wrong logo was a design change because the build was green and no error mentioned resources. The release notes said nothing about branding, so nobody flagged it for two days.
Root cause
Both the app and an integrated SDK defined drawable/splash_logo with identical qualifiers. Resource merge priority silently picked the SDK's file (library order, not intent), so the app shipped the vendor's splash. No duplicate error fired because the merge treated same-qualifier app-vs-library overlap by priority rather than failure in this configuration.
Fix
The host app renamed its splash to brand_splash_host, the library adopted resPrefix brandkit_, and shared onboarding strings moved into a :brand-core module both consume. A lint gate now fails builds on duplicate-resource warnings, and screenshot tests cover the launch screen per flavor.
Key lesson
  • 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.
Production debug guideFive checks from red merge task to verified fix in minutes.5 entries
Symptom · 01
Build fails naming two files with the same resource
→
Fix
Run ./gradlew :app:mergeDebugResources --info and search output for 'Duplicate resources'. It prints both file paths. Decide the canonical owner (app beats library; shared base beats copies), rename or move the other, then rebuild the same task to confirm green.
Symptom · 02
Manifest merger fails on application label, icon, or permission
→
Fix
Open app/build/outputs/logs/manifest-merger-debug-report.txt and find the conflicting element. Add tools:replace='...' listing only the attributes you intend to override, rebuild, and verify the report shows your value winning while library components remain.
Symptom · 03
You suspect silent wrong-value merges or want a full audit
→
Fix
Run ./gradlew :app:lintDebug and open the report; the Correctness:Duplicate* and UnusedResources checks list collisions and dead weight. Fix each duplicate at its source module rather than suppressing, and re-run lint to confirm zero.
Symptom · 04
Failure appears only on incremental builds, clean builds pass
→
Fix
Run ./gradlew clean :app:assembleDebug. If clean passes but incremental fails, suspect stale outputs: check for renamed-but-cached resources, verify Gradle and AGP versions, and confirm no two source sets point at the same res directory.
Symptom · 05
Build is green but a flavor shows the wrong string or icon
→
Fix
Open APK Analyzer (Build > Analyze APK), compare the resource table across flavors, and confirm each name resolves to the intended value. Screenshot-test one screen per flavor; wrong-value merges show up visually when logs look clean.
Duplicate Resource Causes, Checks, and Fixes
Root CauseHow to ConfirmFixPrevention
Same name in main and flavor/librarymerge task names both files; grep finds the twinsRename one side or move shared value to baseresPrefix per module; unique naming policy
Manifest attribute collisionMerged-manifest report flags the conflicttools:replace on the intended attribute onlyMinimize manifest claims in libraries
Unneeded locales/densities merged inAPK analyzer shows unused buckets; lint flagsresConfigs to keep only shipped configsDeclare resConfigs from project start
Stale build cache masking renamesClean build passes while incremental failsClean + rebuild; fix caching configReliable clean CI builds on release branches
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
flavorPaidresvaluesstrings.xml<!-- BEFORE: main/res/values/strings.xml -->Reading the Merge Error
libraryresvaluesstrings.xml<!-- library/build.gradle: enforce prefixed names -->Killing Collisions With resourcePrefix Discipline
appsrcmainAndroidManifest.xml<!-- 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

1
Same type + name + qualifiers from two sources is a duplicate
the merge task names both files.
2
Consolidate shared values into a base module; rename the rest with per-module resPrefix.
3
Scope tools:replace narrowly so manifest overrides don't drop library components.
4
Declare resConfigs to strip unshipped densities and locales, shrinking APKs and conflicts together.
5
Treat silent wrong-value merges as bugs
audit same-name resources even when the build passes.
6
Gate with lint and clean CI builds so collisions fail at the author's desk.

Common mistakes to avoid

5 patterns
×

Generic resource names in library modules (btn_ok, ic_close)

Symptom
Every new app integration collides with a different host app, and each fix is a rename patch instead of a policy.
Fix
Adopt resPrefix (e.g., checkout_ for a :checkout library) so collisions become compile-time impossibilities. Rename existing clashes in one pass and enforce the prefix in review.
×

Copy-pasting the same strings and colors into every flavor

Symptom
Main and flavor define app_name slightly differently, merges conflict, and translators get duplicate keys that drift apart.
Fix
Move shared values into the base module or a :ui-core library and delete the copies. One definition, many consumers — flavor overrides only where behavior truly differs.
×

Assuming the build picks the 'right' duplicate

Symptom
Green builds shipping wrong icons or strings, discovered by QA screenshots or, worse, users in a different flavor.
Fix
Run ./gradlew :app:mergeDebugResources --info and read the 'Duplicate resources' lines naming both files. The winner is arbitrary — treat every duplicate as a bug regardless of which build passed.
×

Using tools:replace as a blanket fix for manifest clashes

Symptom
Library permissions or components silently dropped from the merged manifest, causing runtime SecurityExceptions weeks later.
Fix
Add tools:replace only for the merged attributes you intend to override (e.g., label), keeping the manifest's other values intact. Document why the override exists with a comment.
×

Shipping every density and locale a library bundles

Symptom
Bloated APKs plus drawable clashes across densities that only appear when a library adds a new bucket.
Fix
Declare resConfigs (or resConfig for single-dimension) listing exactly the languages and densities you ship. Watch APK size drop and the duplicate-density conflicts vanish with it.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What causes app:mergeDebugResources duplicate resource errors?
Q02SENIOR
Two libraries ship ic_close. Walk me through the fix.
Q03SENIOR
What does resConfigs do and why does it help?
Q04SENIOR
Explain manifest merger priority and scoped tools:replace.
Q05SENIOR
Design a resource policy so a 20-module app never hits this again.
Q01 of 05JUNIOR

What causes app:mergeDebugResources duplicate resource errors?

ANSWER
Two files define the same resource (type + name + qualifiers) — e.g., main and a flavor both ship string/app_name, or two libraries bundle drawable/icon. The merge task can't pick a winner safely, so it fails listing both files. The fix is renaming one side, consolidating to a shared module, or scoping with qualifiers.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Do resources of different types with the same name conflict?
02
How do density and locale qualifiers affect duplicates?
03
What does tools:replace actually do in the manifest merge?
04
How do I enforce resource prefixes in a library?
05
Can resConfigs shrink my APK as well as fix conflicts?
06
How do I handle translatable strings colliding across modules?
N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Drawn from code that ran under real load.

Follow
✓ Verified
production tested
September 27, 2026
last updated
2,085
articles · all by Naren
🔥

That's Android. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
Kotlin Unresolved Reference After Adding Dependency
1 / 3 · Android
Next
Android Default FirebaseApp Is Not Initialized
→