</>CodeWithKarani

React Native Android 'Duplicate Class': Finding Which Two Dependencies Conflict

Karani GeoffreyKarani Geoffrey6 min read

Your React Native Android build was fine yesterday. You added one library, ran ./gradlew assembleRelease, and now the build dies with a wall of red that starts with Duplicate class and lists a fully-qualified Java class name you have never typed in your life. You did not write that class. Neither did the library you just added. So why is Gradle blaming you?

Here is the trap: the error names a class, but the conflict is between two packages. Gradle is telling you the symptom, not the cause. Most people react by excluding the library they just installed, the build still fails, they exclude another one, and an hour later they have a build.gradle full of superstition and a build that is more broken than when they started.

The fix is boring and reliable: find the two dependency paths that both pull in the conflicting artifact, then force a single version across the whole graph. Excluding at random is not it.

Run ./gradlew app:dependencies and search the output for the artifact the duplicated class belongs to. You will find two paths pulling it in at different coordinates, most often an old Android Support Library version alongside AndroidX. Set android.useAndroidX=true and android.enableJetifier=true together in gradle.properties, add a resolutionStrategy.force to pin one version, then do a clean rebuild (./gradlew clean first) because Gradle's cache can hide whether the fix worked.

The exact error you are seeing

It looks something like this, and the specific class name varies:

Execution failed for task ':app:checkDebugDuplicateClasses'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.CheckDuplicatesRunnable
   > Duplicate class android.support.v4.app.INotificationSideChannel found in modules
     jetified-core-1.9.0-runtime (androidx.core:core:1.9.0) and
     support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)

Read that last part carefully, because it is the entire diagnosis handed to you: the same class exists in androidx.core:core and com.android.support:support-compat. Those are the old and new homes of the same code. One dependency in your tree still asks for the pre-AndroidX Support Library, and another asks for AndroidX, and both contain a class with that name.

Why this happens: the AndroidX migration is only half-done in your graph

In 2018 Google repackaged the Android Support Library into AndroidX. android.support.v4.app.Foo became androidx.core.app.Foo. The code is largely the same, which is exactly why you get a duplicate: two artifacts ship classes that occupy the same namespace after Jetifier rewrites them.

This bites you when some of your dependencies have migrated to AndroidX and some have not. A modern React Native core is AndroidX. But that one older native module you just added, the one that has not been updated since 2019, still declares a dependency on com.android.support. Now your graph contains both worlds, and the merge step refuses to guess which class you meant.

The reason random exclusion fails is that the conflicting artifact is almost never a direct dependency. It is transitive, dragged in two or three levels deep by libraries that never mention it in their name. You cannot fix what you cannot see, and the class name in the error does not tell you which of your libraries is responsible. You have to look at the tree.

app (your project) react-native core (AndroidX) old-native-module (pre-AndroidX) androidx.core:core INotificationSideChannel com.android.support:support-compat INotificationSideChannel Same class, two homes, one merge step that refuses to choose
The duplicated class lives at the bottom. The error names the leaf; the cause is which two branches lead to it.

The fix, step by step

Step 1: Print the dependency tree and find the artifact

From your android/ directory, ask Gradle for the full tree for the app module:

cd android
./gradlew app:dependencies --configuration releaseRuntimeClasspath

That is a large output. Do not read it top to bottom. Take the artifact from the error, in the example above the two are androidx.core:core and com.android.support:support-compat, and search for the older one:

./gradlew app:dependencies --configuration releaseRuntimeClasspath \
  | grep -i "com.android.support"

Gradle prints each dependency indented under its parent, so the lines directly above your match tell you which library dragged it in. That parent is the real culprit, the half-migrated module. Now you know the two paths instead of guessing.

Step 2: Turn on AndroidX and Jetifier, both together

Open android/gradle.properties and make sure both of these are present:

android.useAndroidX=true
android.enableJetifier=true

These two are a pair and must be set together. useAndroidX=true tells the build your project uses AndroidX. enableJetifier=true tells Gradle to rewrite the bytecode of old Support-Library dependencies on the fly so they call AndroidX classes instead. Set useAndroidX without enableJetifier and the old module keeps referencing android.support classes that no longer exist at runtime; set neither and you keep the duplicate. One without the other reintroduces the very conflict you are trying to kill.

Step 3: Force one version across every configuration

Now pin the winner. In android/app/build.gradle, add a global resolution strategy. This is the part the one-line StackOverflow answers skip, and it is the part that actually holds:

// android/app/build.gradle
configurations.all {
    resolutionStrategy {
        force 'androidx.core:core:1.9.0'
        // add other AndroidX artifacts here if the tree shows more conflicts
    }
}

configurations.all matters. Applying force to a single configuration leaves the others free to resolve the old version, and the merge step reads more than one configuration. Forcing across all of them is what guarantees a single version everywhere. Prefer this over piling up exclude group: lines on individual dependencies, which is whack-a-mole: exclude one path and the artifact often re-enters through a path you did not exclude.

Step 4: Clean, then rebuild from scratch

Gradle caches resolved dependencies aggressively. An incremental build can succeed or fail based on stale cache state and tell you nothing about whether your change worked. Force a real rebuild:

./gradlew clean
./gradlew assembleRelease

If you are still suspicious the cache is lying to you, ./gradlew --stop kills the Gradle daemon so the next build starts cold.

Verifying the conflict is actually gone

Two checks. First, the old coordinates should no longer appear in the resolved tree at all:

./gradlew app:dependencies --configuration releaseRuntimeClasspath \
  | grep -i "com.android.support"

Expected output: nothing. If Jetifier did its job, every Support-Library reference has been rewritten to AndroidX and the group is gone. If lines still appear, the parent library was excluded by Jetifier config (some projects add android.jetifier.ignorelist) or the module is one Jetifier cannot rewrite; go back to Step 1 and identify it precisely.

Second, the build itself passes the duplicate-class check:

BUILD SUCCESSFUL in 1m 12s

What people get wrong

Excluding the library named in the error. The class name in Duplicate class ... belongs to whichever module happened to sort first. It is not "the bad dependency". Excluding it usually removes the AndroidX copy and leaves the ancient one, which then fails at runtime with ClassNotFoundException instead of at build time. You have not fixed anything; you have moved the crash to your users.

Adding exclude to every dependency until it builds. This can produce a build that compiles and then crashes on launch because you excluded a class something actually needed. Exclusions are a scalpel for a known, specific transitive you have identified in the tree, not a shotgun.

Setting enableJetifier=false to "simplify". Advice floating around says Jetifier is deprecated and you should turn it off. That is only true once every dependency you use is natively AndroidX. If even one is not, disabling Jetifier is what creates the duplicate. Turn it off only after the verification grep returns nothing with it still on.

Trusting an incremental build. "It builds now" after editing build.gradle without a clean means very little. The dependency cache can serve you the pre-change resolution. Always clean-rebuild before you believe the fix, the same discipline that a React Native version mismatch demands.

When it is still broken

  • The conflict is not Support vs AndroidX. The same mechanism happens with other doubly-included artifacts, commonly com.google.android.gms or two versions of an OkHttp/Guava transitive. The method is identical: grep the tree for the artifact, find the two paths, force one version.
  • Jetifier cannot rewrite the module. Some old .aar files resist rewriting. The durable fix is to replace or update that native module to an AndroidX-native version; check its repo for a newer release or a maintained fork.
  • Force is being ignored. If the version keeps resolving to something else, another module is calling strictly or a platform BOM is pinning it. Run ./gradlew app:dependencyInsight --dependency androidx.core:core --configuration releaseRuntimeClasspath to see exactly why Gradle chose the version it did.
  • Only release builds fail. The duplicate check runs per variant. If debug passes and release fails, run the dependency and grep commands against releaseRuntimeClasspath specifically, because the variants can resolve differently.

The whole skill here is refusing to act on the class name and instead reading the tree. Once you can see the two paths, the fix is three lines of Gradle and a clean build, every time.

Frequently asked questions

Why does the Duplicate class error name a class I never wrote?
The class lives inside a transitive dependency, usually two or three levels deep, that two of your libraries both pull in at conflicting versions. Gradle reports the leaf class where the collision surfaces, not the packages responsible. Run ./gradlew app:dependencies and search for the artifact that class belongs to to find the two paths.
Do I need both android.useAndroidX and android.enableJetifier?
Yes, together. useAndroidX=true declares your project uses AndroidX; enableJetifier=true rewrites old Support-Library dependencies to call AndroidX classes at build time. Setting one without the other commonly reintroduces the exact duplicate-class conflict you are trying to fix.
Should I use exclude or resolutionStrategy.force to fix a duplicate class?
Prefer resolutionStrategy.force inside configurations.all to pin one version across every configuration. Piecemeal exclude directives on individual dependencies are whack-a-mole, because the artifact usually re-enters through another transitive path you did not exclude, and can leave you with a runtime ClassNotFoundException.
Why does my build still fail after I fixed build.gradle?
Gradle caches resolved dependencies, so an incremental build can succeed or fail on stale state and hide whether your change took effect. Run ./gradlew clean before assembleRelease, and ./gradlew --stop if you suspect the daemon is serving cached resolution.
#React Native#Android#Gradle#AndroidX#Jetifier
Keep reading

Related articles