Skip to content

fix(android): skip explicit Kotlin plugin when AGP registers the kotlin extension - #1394

Open
gabrieldonadel wants to merge 3 commits into
DataDog:developfrom
gabrieldonadel:fix/agp9-built-in-kotlin
Open

gabrieldonadel wants to merge 3 commits into
DataDog:developfrom
gabrieldonadel:fix/agp9-built-in-kotlin

Conversation

@gabrieldonadel

@gabrieldonadel gabrieldonadel commented Sep 2, 2026 •

Copy link
Copy Markdown

Problem

Android Gradle Plugin 9 ships built-in Kotlin support and enables it by default, so
AGP registers the kotlin extension itself. When a library also applies kotlin-android
explicitly, the two collide and configuration fails before anything compiles. AGP
words it two ways, both the same problem:

> Failed to apply plugin 'kotlin-android'.
   > Cannot add extension with name 'kotlin', as there is an extension already registered with that name.
> The 'kotlin-android' plugin is no longer required for Kotlin support since AGP 9.0.

The apply is unconditional in these files, so on an AGP 9 project they cannot be built
at all. There is no consumer-side workaround short of patching the file — setting
android.builtInKotlin=false project-wide just to build one dependency is not a
reasonable ask, and that escape hatch is removed in AGP 10.

Change

Apply the plugin only when nothing has registered the kotlin extension yet:

if (project.extensions.findByName('kotlin') == null) {
    apply plugin: 'kotlin-android'
}

Files changed:

  • packages/core/android/build.gradle
  • packages/internal-testing-tools/android/build.gradle
  • packages/react-native-session-replay/android/build.gradle
  • packages/react-native-webview/android/build.gradle

Why this shape

Earlier revisions of this PR derived the answer from the AGP version and the
android.builtInKotlin property. Asking for the extension directly is better on
three counts:

  • It tests the condition that actually fails. The collision is "something already
    registered kotlin", so that is what the guard checks. No AGP version table to keep
    in sync.
  • It cannot be fooled. android.builtInKotlin is a global switch, but built-in
    Kotlin can also be enabled per module with the com.android.built-in-kotlin plugin,
    so the global value can disagree with the module. Reading
    com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION has its own trap: it resolves
    against the buildscript classpath, which is not always the AGP that ends up running.
  • It covers AGP 10 for free. The android.builtInKotlin opt-out is removed there,
    and this guard needs no special case for it.
AGP android.builtInKotlin kotlin extension explicit apply
8.x unset or false absent yes (unchanged)
9.x unset or true registered by AGP no
9.x false absent yes
10+ n/a (removed) registered by AGP no

The guard sits after apply plugin: 'com.android.library' in every file it touches,
so AGP has already registered its extensions by the time it runs. I checked that
ordering per file rather than assuming it.

What I verified, and what I did not

  • Verified end to end on a real Expo SDK 58 / React Native 0.87 project with AGP
    9.2.1 and Gradle 9.4.1: :app:assembleDebug succeeds both with
    -Pandroid.newDsl=true -Pandroid.builtInKotlin=true and with both flags off. That
    run used the earlier version-based guard; the extension guard in this revision is
    the shape now used across the rest of this sweep and already merged in several of
    those repos.
  • Syntax-checked these files with Groovy's Phases.CONVERSION.
  • Not run: this repo's own CI or example app. A CI run is the real confirmation
    and I could not do that from outside.

Found while sweeping 157 popular React Native libraries for AGP 9 new-DSL compatibility.
34 failed with the new DSL enabled, and 29 of those failed on exactly this — it is the
most common blocker by a wide margin.

This guard shape was suggested by the RevenueCat maintainers on
RevenueCat/react-native-purchases#1934,
who had already hit the same issue in their Capacitor and Flutter SDKs. I have
since standardised on it across this sweep.

Copilot AI lite review requested due to automatic review settings September 2, 2026 20:35
@gabrieldonadel
gabrieldonadel requested a review from a team as a code owner September 2, 2026 20:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The change is narrowly scoped to conditional plugin application and preserves AGP <= 8 behavior while preventing the known AGP 9+ configuration failure.

Pull request overview

This PR prevents Android builds from failing on Android Gradle Plugin (AGP) 9+ by avoiding an explicit kotlin-android plugin apply when AGP’s built-in Kotlin support is already enabled, while preserving the existing behavior for AGP 8 and older.

Changes:

  • Added a shouldApplyKotlinPlugin() guard that checks the AGP major version and android.builtInKotlin to decide whether to apply kotlin-android.
  • Replaced unconditional apply plugin: 'kotlin-android' with a conditional apply in the affected package Gradle scripts.
File summaries
File Description
packages/core/android/build.gradle Conditionally applies kotlin-android to avoid AGP 9 built-in Kotlin collisions.
packages/internal-testing-tools/android/build.gradle Adds AGP/version + property gate around kotlin-android plugin application.
packages/react-native-session-replay/android/build.gradle Avoids double-applying Kotlin on AGP 9+ by guarding the Kotlin plugin apply.
packages/react-native-webview/android/build.gradle Adds the same conditional Kotlin plugin apply to prevent configuration-phase failures on AGP 9+.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@gabrieldonadel
gabrieldonadel force-pushed the fix/agp9-built-in-kotlin branch from fcbc2a7 to 82e70b3 Compare September 2, 2026 21:01
Copilot AI review requested due to automatic review settings September 2, 2026 21:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The guard is consistent with existing AGP version checks in these scripts and prevents the known AGP 9+ Kotlin plugin double-application failure without affecting AGP ≤ 8 behavior.

Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

…Kotlin

AGP 9 enables built-in Kotlin by default and applies the Kotlin plugin
itself. Applying it again fails configuration with "Cannot add extension
with name 'kotlin'". Guard the explicit apply so it only runs when AGP is
not providing Kotlin: AGP 8 and older, or AGP 9 with
android.builtInKotlin=false. AGP 10 removes that opt-out, so built-in
Kotlin is always active there and the explicit apply must never run.
@cdn34dd
cdn34dd force-pushed the fix/agp9-built-in-kotlin branch from 82e70b3 to dea1cc1 Compare September 7, 2026 14:55
Copilot AI review requested due to automatic review settings September 7, 2026 14:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It changes build configuration behavior across multiple AGP versions (including 9/10) and warrants a human validation pass (e.g., CI matrix / example builds) before approval.

Review details

Suppressed comments (1)

packages/core/android/build.gradle:55

  • The same shouldApplyKotlinPlugin() helper is duplicated across multiple module build.gradle files. That increases the chance of the logic diverging over time (especially around AGP 9/10 edge cases). Consider extracting this into a shared Gradle script (e.g. under packages/*/android/gradle/ or a root gradle file) and apply from: it in each module.
def shouldApplyKotlinPlugin() {
  def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0].toInteger()
  if (agpMajor <= 8) {
    return true
  }
  // AGP 10 removes the opt-out: built-in Kotlin is always active there.
  if (agpMajor >= 10) {
    return false
  }
  def propertyVal = providers.gradleProperty("android.builtInKotlin").orNull
  def builtInKotlinEnabled = propertyVal != null ? propertyVal.toBoolean() : true
  return !builtInKotlinEnabled
}
  • Files reviewed: 4/4 changed files
  • Comments generated: 4
  • Review effort level: Lite

Comment thread packages/core/android/build.gradle Outdated
Comment thread packages/internal-testing-tools/android/build.gradle Outdated
Comment thread packages/react-native-session-replay/android/build.gradle Outdated
Comment thread packages/react-native-webview/android/build.gradle Outdated
@datadog-datadog-prod-us1-2

This comment has been minimized.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 7, 2026 20:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It changes Gradle/AGP version-dependent build behavior across multiple packages without a repository CI run to validate the full matrix of supported Android build environments.

Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 14, 2026 13:57
@gabrieldonadel gabrieldonadel changed the title fix(android): skip explicit Kotlin plugin when AGP provides built-in Kotlin fix(android): skip explicit Kotlin plugin when AGP registers the kotlin extension Sep 14, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The name-only Kotlin extension check can suppress the required plugin when an unrelated extension is present.

Review details

Suppressed comments (4)

packages/core/android/build.gradle:47

  • This name-only test can be true for any extension called kotlin, not just AGP's built-in Kotlin. In an AGP <= 8 consumer that pre-registers another kotlin extension (or in AGP 9 with built-in Kotlin disabled), this skips the only kotlin-android apply even though this module contains Kotlin sources, leaving them unconfigured. Use the AGP-version/property check described in the PR, or otherwise verify the built-in plugin rather than treating the extension name alone as proof.
if (project.extensions.findByName('kotlin') == null) {
  apply plugin: 'kotlin-android'
}

packages/internal-testing-tools/android/build.gradle:42

  • This name-only test can be true for any extension called kotlin, not just AGP's built-in Kotlin. In an AGP <= 8 consumer that pre-registers another kotlin extension (or in AGP 9 with built-in Kotlin disabled), this skips the only kotlin-android apply even though this module contains Kotlin sources, leaving them unconfigured. Use the AGP-version/property check described in the PR, or otherwise verify the built-in plugin rather than treating the extension name alone as proof.
if (project.extensions.findByName('kotlin') == null) {
  apply plugin: 'kotlin-android'
}

packages/react-native-session-replay/android/build.gradle:45

  • This name-only test can be true for any extension called kotlin, not just AGP's built-in Kotlin. In an AGP <= 8 consumer that pre-registers another kotlin extension (or in AGP 9 with built-in Kotlin disabled), this skips the only kotlin-android apply even though this module contains Kotlin sources, leaving them unconfigured. Use the AGP-version/property check described in the PR, or otherwise verify the built-in plugin rather than treating the extension name alone as proof.
if (project.extensions.findByName('kotlin') == null) {
  apply plugin: 'kotlin-android'
}

packages/react-native-webview/android/build.gradle:43

  • This name-only test can be true for any extension called kotlin, not just AGP's built-in Kotlin. In an AGP <= 8 consumer that pre-registers another kotlin extension (or in AGP 9 with built-in Kotlin disabled), this skips the only kotlin-android apply even though this module contains Kotlin sources, leaving them unconfigured. Use the AGP-version/property check described in the PR, or otherwise verify the built-in plugin rather than treating the extension name alone as proof.
if (project.extensions.findByName('kotlin') == null) {
  apply plugin: 'kotlin-android'
}
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@gabrieldonadel

Copy link
Copy Markdown
Author

Pushed a revision that changes how the guard decides, not what it does.

Before, it derived the answer from the AGP version and the android.builtInKotlin
property. It now asks the question directly:

if (project.extensions.findByName('kotlin') == null) {
    apply plugin: 'kotlin-android'
}

Three reasons this is the better shape:

  • It tests the condition that actually fails, so there is no AGP version table to keep
    in sync.
  • It cannot be fooled. android.builtInKotlin is global, but built-in Kotlin can also
    be switched on per module with com.android.built-in-kotlin, and
    com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION resolves against the buildscript
    classpath, which is not always the AGP that ends up running.
  • It covers AGP 10, where the android.builtInKotlin opt-out is removed, with no
    special case.

This shape was suggested by the RevenueCat maintainers on
RevenueCat/react-native-purchases#1934
and is what the rest of this sweep now uses. Behaviour on AGP 8 and older is unchanged.
Same files as before; title and description updated to match. Sorry for the extra
notification.

Replace the AGP version / android.builtInKotlin check with a direct test for
the registered kotlin extension. The version check reads
com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION, which can resolve to a
different classpath entry than the AGP actually in use, and the global
android.builtInKotlin property can be overridden per module by the
com.android.built-in-kotlin plugin -- so both inputs can disagree with
reality. Asking whether the kotlin extension exists tests the condition that
actually fails, needs no AGP version table, and covers AGP 10 where the
opt-out is removed.
@gabrieldonadel
gabrieldonadel force-pushed the fix/agp9-built-in-kotlin branch from bfff093 to 7397588 Compare September 18, 2026 10:21

@marco-saia-datadog marco-saia-datadog left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants