🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
// DOC-DERIVED — canonical Gradle/Android paste; artifact versions vary but sentence shape is fixed:
Duplicate class com.google.common.collect.ListenableFuture found in modules guava-26.0-android.jar (com.google.guava:guava:26.0-android) and listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava)
Go to the documentation to learn how to <a href="d.android.com/r/tools/classpath-sync-errors">fix dependency resolution errors.</a>⚡ Quick Fix Works 80% of the time
Exclude the empty listenablefuture stub everywhere it leaks in, then pin one real Guava version via a constraint so only full Guava provides its classes.
dependencies {
implementation("com.google.guava:guava:33.2.1-android")
configurations.all {
exclude(group = "com.google.guava", module = "listenablefuture")
}
}🧠 Why this Happens
Tap to expand the deep technical explanation
The listenablefuture-9999.0-empty-to-avoid-conflict-with-guava artifact is intentionally an empty jar whose POM advertises the ListenableFuture API so old libraries depending on com.google.guava:listenablefuture resolve without dragging all of Guava into Android apps. When BOTH the stub and real Guava end up on one compile classpath, two modules provide identical FQCNs; Gradle’s duplicate-class check (and javadoc-level resolution) aborts rather than silently ordering them like the old classpath world did.
The HITEC City Parking Spot Analogy:
Two moving companies both claim box #7 of your shipment. Rather than guess which crew owns which box, the foreman stops the whole move until the manifest lists exactly one owner per box.
🔁 How to Reproduce Confirm this is your error
Add dependencies on both com.google.guava:guava and any library transitively pulling com.google.guava:listenablefuture, then run assembleDebug — the duplicate-class failure prints with both jar names. DOC-DERIVED — deterministic build-time failure.
🛠️ Solutions (5 Ways to Fix)
Exclude the stub globally and depend on real Guava once
👉 Use this as the standard fix — the stub exists solely to be excluded when full Guava is present.
A configuration-wide exclusion removes every path to the empty jar while your explicit guava coordinate supplies all classes. One source of truth, no duplicates, no behavior change for consumers of the ListenableFuture API.
// Groovy DSL
configurations.all {
exclude group: 'com.google.guava', module: 'listenablefuture'
}
dependencies {
implementation 'com.google.guava:guava:33.2.1-android'
}Trace the puller with dependencyInsight before excluding blindly
👉 Use this to document WHY the stub arrives — knowledge that survives team handoffs.
dependencyInsight shows every path from your build to the offending module, including which direct dependency drags it. Excluding at that edge keeps the fix surgical instead of global.
./gradlew dependencyInsight --dependency listenablefuture --configuration debugRuntimeClasspathAlign versions with a platform/BOM instead of scattered pins
👉 Use this when multiple Guava-family artifacts (failureaccess, jsr305, animal-sniffer) drift apart.
Importing the Guava BOM (or a version catalog) makes every module agree on companion versions, preventing tomorrow’s duplicate-class report about a different pair of jars.
dependencies {
implementation(platform("com.google.guava:guava-bom:33.2.1-android"))
implementation("com.google.guava:guava")
}Fail fast on future duplicates with a dedicated check task
👉 Use this to convert silent classpath-order luck into loud build failures forever.
Android projects already get AGP’s checkDebugDuplicateClasses; JVM Gradle projects can add a small task that indexes every .class entry across runtimeClasspath jars and fails when two artifacts own the same file. Maven users get the same guarantee from Enforcer’s BanDuplicateClasses (solution 5).
// build.gradle.kts
import java.util.zip.ZipFile
tasks.register("checkDuplicateClasses") {
doLast {
val owners = mutableMapOf<String, MutableSet<String>>()
configurations.runtimeClasspath.get().files.forEach { jar ->
ZipFile(jar).use { zf ->
zf.entries().asSequence()
.filter { it.name.endsWith(".class") }
.forEach { owners.getOrPut(it.name) { mutableSetOf() }.add(jar.name) }
}
}
val dups = owners.filterValues { it.size > 1 }
if (dups.isNotEmpty()) {
dups.forEach { (cls, who) -> logger.error("DUPLICATE: $cls in $who") }
throw GradleException("Duplicate classes found across ${dups.size} entries")
}
}
}
tasks.named("build") { dependsOn("checkDuplicateClasses") }Maven equivalent: exclusions plus enforcer banDuplicateClasses
👉 Use this if the project is Maven but the same Guava/stub clash appears there.
Identical disease, different tooling: exclude the stub transitive and let Enforcer fail packaging when any class file exists in more than one artifact.
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>listenablefuture</artifactId>
</exclusion>
</exclusions>
<!-- plus BanDuplicateClasses rule in maven-enforcer-plugin -->📋 Version Notes
Plain javac tolerated duplicates silently by classpath order — bugs shipped unnoticed.
Gradle 6+ surfaces duplicates loudly; stub artifact born 2018 era.
Same build-time guard; Android AGP enforces via checkReleaseDuplicateClasses.
Unchanged; version catalogs make single-source pinning easier.
🛡️ How to Prevent This Next Time
Centralize versions in catalogs/BOMs, run duplicate-class detection in CI for every variant, and review new third-party dependencies with dependencyInsight before they enter the graph.