Summary
Android builds of a project that uses Unity IAP 5 fail during the Gradle duplicate-class check with a long list of duplicate kotlin.* classes coming from two different kotlin-stdlib jars. This is a Kotlin standard-library version conflict that originates in Unity IAP 5's bundled dependency on Google Play Billing, not anything the developer added. It reproduces on a fresh, empty project with only IAP 5 installed.
Fix it by forcing a single Kotlin version and excluding the legacy split artifacts via a custom Gradle template.
Symptoms
The build fails with output similar to:
Execution failed for task ':launcher:checkDebugDuplicateClasses'.
> Duplicate class kotlin.collections.jdk8.CollectionsJDK8Kt found in modules
kotlin-stdlib-1.8.22.jar (org.jetbrains.kotlin:kotlin-stdlib:1.8.22) and
kotlin-stdlib-jdk8-1.6.21.jar (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21)
Duplicate class kotlin.internal.jdk7.JDK7PlatformImplementations found in modules
kotlin-stdlib-1.8.22.jar ... and kotlin-stdlib-jdk7-1.6.21.jar ...
... (many more kotlin.*.jdk7 / kotlin.*.jdk8 classes) ...
Key indicators:
- The failing task is
checkDebugDuplicateClassesorcheckReleaseDuplicateClasses
(variant depends on build type). - Duplicate classes are all in
kotlin.*.jdk7/kotlin.*.jdk8packages. - The two colliding modules are
kotlin-stdlib:1.8.22(or another 1.8+ version)
andkotlin-stdlib-jdk7/kotlin-stdlib-jdk8:1.6.21. - The log may show
jetified-kotlin-stdlib-jdk8-1.6.21.
Root cause
-
Unity IAP 5 declares
com.android.billingclient:billing:9.0.0as an Android
dependency. In IAP 5.4.1 this is injected directly into the generated Gradle
project by the package's self-declared-dependency injector
(Plugins/UnityPurchasing/Android/IAPResolver/IAPAndroidDependencies.cs), which
also sets:android.useAndroidX=true android.enableJetifier=true -
Play Billing 9.0.0's own transitive dependencies are internally
version-skewed. Its POM declares no Kotlin directly, but it pulls in a mix of
modern and 4-year-old Android libraries:-
Modern side (confirmed):
billing:9.0.0→androidx.core:core:1.15.0→org.jetbrains.kotlin:kotlin-stdlib:1.8.22(the modern merged stdlib). -
Legacy side: Billing also pulls 2020–2021-era artifacts
(androidx.activity:1.2.3, and via Play Servicesandroidx.fragment:1.1.0/androidx.core:1.2.0); somewhere down that older branch the splitkotlin-stdlib-jdk7:1.6.21andkotlin-stdlib-jdk8:1.6.21get resolved.
-
Modern side (confirmed):
- As of Kotlin 1.8.0, the
-jdk7and-jdk8artifacts were merged into the
mainkotlin-stdlib. So the same classes now exist in both the 1.8.22 jar and the 1.6.21 jdk7/jdk8 jars. - Gradle's
checkDuplicateClassestask detects the collision and fails the build. IAP injectsandroid.enableJetifier=true, which "jetifies" the legacy jars.
That is why they appear asjetified-kotlin-stdlib-jdk8-1.6.21in the log.
This is not caused by anything in the user's project. It is triggered by
installing Unity IAP 5 alone and reproduces on a brand-new empty project. The version skew ships inside the Play Billing dependency that the IAP package injects.
The Unity Android engine and Gradle templates do not declare any Kotlin dependency
Solution (recommended, works with or without EDM4U)
Align all Kotlin stdlib artifacts to one modern version using a custom Base Gradle template. Because the duplicate-class check runs in the :launcher module, this must live in the root build.gradle, where an allprojects block cascades to every module.
This approach is preferred over excluding the jdk7/jdk8 artifacts (see Why align instead of exclude?) because it keeps the kotlin-stdlib-jdk7/jdk8 coordinates resolvable, so an older third-party plugin
that explicitly depends on them still resolves cleanly, while eliminating the duplicate classes.
- In Unity:
Edit ▸ Project Settings ▸ Player ▸ Android ▸ Publishing Settings ▸ Build→ enable Custom Base Gradle Template.
This generatesAssets/Plugins/Android/baseProjectTemplate.gradle. -
Add the following block to the end of that file:
allprojects { configurations.all { resolutionStrategy { force 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22' force 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22' force 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22' } } }Do NOT add a
repositories { }block here — modern Unity 6 keeps
repositories insettingsTemplate.gradleunderdependencyResolutionManagement
withRepositoriesMode.FAIL_ON_PROJECT_REPOS, and a project-level repository
would fail the build. Adding onlyconfigurationsis safe.
Also leave the**BUILD_SCRIPT_DEPS**token untouched — Unity replaces it at
build time. - Do a clean build (delete the build output, or use Build Settings → Clean
Build) so the Gradle project regenerates with the new root template, then
rebuild.
Forcing all three artifacts to 1.8.22 upgrades the legacy 1.6.21 jdk7/jdk8 jars to their modern form. At 1.8+, kotlin-stdlib-jdk7/jdk8 are empty transitional artifacts that simply depend on kotlin-stdlib (verified in their POMs), so they contribute no classes. The duplicates disappear, and no coordinate goes missing.
Full example baseProjectTemplate.gradle
plugins {
id 'com.android.application' version '8.10.0' apply false
id 'com.android.library' version '8.10.0' apply false
**BUILD_SCRIPT_DEPS**
}
tasks.register('clean', Delete) {
delete rootProject.layout.buildDirectory
}
allprojects {
configurations.all {
resolutionStrategy {
force 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22'
force 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22'
force 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22'
}
}
}
Why align instead of exclude?
An alternative fix would be to exclude the legacy artifacts entirely:
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
This also works and is safe in most projects (the jdk7/jdk8 classes were merged into kotlin-stdlib, so nothing is lost at runtime, and exclude only prunes the tree, it does not by itself cause "could not resolve" errors for other libraries; those come from forcing a non-existent version or from strict constraints).
However, if an older third-party plugin explicitly declares a dependency on kotlin-stdlib-jdk7/kotlin-stdlib-jdk8, removing the artifact outright is a slightly higher-risk choice than simply upgrading it. Version alignment (the recommended block above) keeps the coordinates present as empty shims, so it is the
safer default. Use exclude only if you specifically want those artifacts gone.
Alternative: Kotlin BOM
For central control of the Kotlin version you can import the Kotlin BOM instead of listing each force; it aligns all kotlin-stdlib* artifacts to one version:
dependencies {
implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.22')
}
The resolutionStrategy.force block is simpler to apply across Unity's generated modules from the base template, so it is used as the primary solution here.
Alternative: EDM4U (External Dependency Manager) projects
Unity IAP 5.4.1 does not use EDM4U; it injects billing via C#. But if your project also has EDM4U (e.g. from Ads or another SDK), you can instead exclude the legacy artifacts from the billing dependency in a *Dependencies.xml:
<dependencies>
<androidPackages>
<androidPackage spec="com.android.billingclient:billing:9.0.0">
<!-- prevent the legacy split stdlib from being pulled in -->
</androidPackage>
</androidPackages>
</dependencies>
The Gradle-template solution above is preferred because it works regardless of whether EDM4U is present and does not depend on resolver ordering.
Notes
- Do not modify auto-generated files like
Assembly-CSharp.csproj; all Android
Gradle changes go through the template files underAssets/Plugins/Android/. - If a future Play Billing / IAP release bumps Kotlin, update the
forceversion to
match the version billing brings in (check thecheckDuplicateClasseslog or the:launcher:dependenciesoutput).