Summary
Seven Android plugins decide whether to apply the Kotlin Gradle Plugin (KGP) based on the AGP major version alone:
val agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.substringBefore('.').toInt()
if (agpMajor < 9) {
apply(plugin = "org.jetbrains.kotlin.android")
}
This assumes AGP 9 always means built-in Kotlin is active. That is not true. AGP 9 enables built-in Kotlin only when android.builtInKotlin is not explicitly false — and android.builtInKotlin=false is exactly what flutter create writes into every new app's android/gradle.properties (via the Flutter migrator), and what every example app in this repository currently sets.
So on AGP 9 + android.builtInKotlin=false the guard is false, the plugin does not apply KGP, and AGP does not provide Kotlin either. Nothing compiles the plugin's Kotlin sources, and configuration fails a few lines below at the unconditional extension lookup:
* Where:
Build file '.../device_info_plus/android/build.gradle.kts' line: 35
* What went wrong:
Extension of type 'KotlinAndroidProjectExtension' does not exist. Currently registered extension types:
[ExtraPropertiesExtension, FlutterExtension, VersionCatalogsExtension, BasePluginExtension, SourceSetContainer,
ReportingExtension, JavaToolchainService, JavaPluginExtension, LibraryExtension, LibraryAndroidComponentsExtension,
LintLifecycleExtension, NamedDomainObjectContainer<BaseVariantOutput>]
Why this is not visible yet
Flutter's Gradle plugin applies kotlin-android on behalf of any Android subproject whose build script does not appear to declare KGP. It decides that by regex over the build file text, and the Kotlin-DSL regex (FlutterPluginUtils.kgpRegexKotlin) only matches plugins { } blocks:
internal val kgpRegexKotlin =
"""(?m)^[ \t]*plugins[ \t]*\{[^{}]*?(?<=[\n{])[ \t]*(?:id|alias)[ \t]*\(\s*(['"](?:kotlin-android|org\.jetbrains\.kotlin\.android)['"]|libs\.plugins\.(?:android|kotlin)\.android)\s*\)(?=[ \t]*(\n|${'$'}|\}))"""
.toRegex()
It does not match the imperative apply(plugin = "…") form these plugins use. So Flutter does not see the declaration, applies KGP itself, and accidentally papers over the broken guard.
Note the asymmetry: the Groovy counterpart kgpRegexGroovy does match apply plugin: '…'. That is precisely why this bug has already been hit in a Groovy-based plugin but not here — see the prior art below.
This is load-bearing accident, not a contract. Flutter's own tooling already warns that this fallback is going away:
WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP): …
Future versions of Flutter will fail to build if your app uses plugins that apply KGP.
Reproduction
Verified on AGP 9.1.0 / Kotlin 2.4.0 / Gradle 9.3.1, Flutter 3.44.8 stable, with android.builtInKotlin=false.
Because Flutter's fallback currently masks the bug, the decisive reproduction removes that mask. In a local Flutter SDK checkout, extend kgpRegexKotlin in packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt so it also matches the imperative form, by prefixing this alternative to the existing pattern:
(?m)^[ \t]*apply[ \t]*\(\s*plugin[ \t]*=[ \t]*(['"])(?:kotlin-android|org\.jetbrains\.kotlin\.android)\1\s*\)|
Then, in any of the affected packages' examples, bump the example to AGP 9 and run flutter build apk --debug with android.builtInKotlin=false. The build fails with the KotlinAndroidProjectExtension does not exist error above. Reverting the SDK edit makes it build again — confirming the plugin currently depends on Flutter's fallback rather than on its own guard.
Evidence that the condition is wrong, not just unlucky
android.builtInKotlin defaults to enabled in AGP 9. From com/android/build/gradle/options/BooleanOption in gradle-9.1.0.jar:
3557: ldc_w // String BUILT_IN_KOTLIN
3560: bipush 114
3562: ldc_w // String android.builtInKotlin
3565: iconst_1 <-- defaultValue = true
3566: new // class FeatureStage$SoftlyEnforced
3570: getstatic // DeprecationTarget.VERSION_10_0
So: absent ⇒ built-in Kotlin on; explicit false ⇒ off. The version check alone cannot tell those apart. (It is also SoftlyEnforced with removal targeted at AGP 10, so opting out is temporary.)
Proposed fix
Guard on the same condition AGP itself uses — built-in Kotlin is on when AGP >= 9 and android.builtInKotlin is not explicitly false:
val agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.substringBefore('.').toInt()
// AGP 9 provides Kotlin support natively unless the consuming app opts out with
// android.builtInKotlin=false, which is what `flutter create` writes by default.
val builtInKotlinEnabled =
agpMajor >= 9 &&
(providers.gradleProperty("android.builtInKotlin").orNull?.toBoolean() ?: true)
if (!builtInKotlinEnabled) {
apply(plugin = "org.jetbrains.kotlin.android")
}
The project.extensions.configure(KotlinAndroidProjectExtension::class.java) { … } block below stays unchanged: AGP 9's built-in Kotlin registers that extension, so it resolves under both branches (confirmed on a real AGP 9.1 build with android.builtInKotlin=true).
On AGP 8 the behaviour is provably identical to today: agpMajor >= 9 is false, so builtInKotlinEnabled is false and KGP is applied exactly as before.
Note on prior art
app_settings 8.0.3 hit this exact failure and fixed it (spencerccf/app_settings#270); its android/build.gradle carries a long comment documenting the mechanism. Its fix is not worth copying verbatim: it reads the property with a ?: 'false' default and ignores the AGP version, so on an AGP 9 project that never sets the property it applies KGP into a build where built-in Kotlin is already active, which throws:
The 'org.jetbrains.kotlin.android' plugin is no longer required for Kotlin support since AGP 9.0.
Flutter apps always write the property, so that case is masked there — but the version above is correct in both cases.
Affected packages
android_alarm_manager_plus
battery_plus
device_info_plus
network_info_plus
package_info_plus
sensors_plus
share_plus
android_intent_plus uses a Groovy build.gradle with no Kotlin sources and is not affected.
Verification performed
| Scenario |
Result |
Patched Flutter regex, AGP 9, builtInKotlin=false, before fix |
Fails with KotlinAndroidProjectExtension does not exist |
Patched Flutter regex, AGP 9, builtInKotlin=false, after fix |
APK built — all 7 packages |
AGP 9, builtInKotlin=true |
APK built; KGP correctly not applied |
| Stock AGP 8.12.1 (current pin) |
APK built — all 7 packages |
Additionally, after the fix, Flutter's warning lists the plugin under "plugins that apply KGP", which means the patched regex did detect the declaration and Flutter did not apply KGP as a fallback — the plugin stands on its own.
As a control, the fix was reverted in a single package (share_plus) and the AGP 9 build failed identically, confirming the passing results are meaningful rather than vacuous.
Per CONTRIBUTING, this is filed as one issue with a proposal, with one branch and pull request per package to follow.
Summary
Seven Android plugins decide whether to apply the Kotlin Gradle Plugin (KGP) based on the AGP major version alone:
This assumes AGP 9 always means built-in Kotlin is active. That is not true. AGP 9 enables built-in Kotlin only when
android.builtInKotlinis not explicitlyfalse— andandroid.builtInKotlin=falseis exactly whatflutter createwrites into every new app'sandroid/gradle.properties(via the Flutter migrator), and what every example app in this repository currently sets.So on AGP 9 +
android.builtInKotlin=falsethe guard isfalse, the plugin does not apply KGP, and AGP does not provide Kotlin either. Nothing compiles the plugin's Kotlin sources, and configuration fails a few lines below at the unconditional extension lookup:Why this is not visible yet
Flutter's Gradle plugin applies
kotlin-androidon behalf of any Android subproject whose build script does not appear to declare KGP. It decides that by regex over the build file text, and the Kotlin-DSL regex (FlutterPluginUtils.kgpRegexKotlin) only matchesplugins { }blocks:It does not match the imperative
apply(plugin = "…")form these plugins use. So Flutter does not see the declaration, applies KGP itself, and accidentally papers over the broken guard.Note the asymmetry: the Groovy counterpart
kgpRegexGroovydoes matchapply plugin: '…'. That is precisely why this bug has already been hit in a Groovy-based plugin but not here — see the prior art below.This is load-bearing accident, not a contract. Flutter's own tooling already warns that this fallback is going away:
Reproduction
Verified on AGP 9.1.0 / Kotlin 2.4.0 / Gradle 9.3.1, Flutter 3.44.8 stable, with
android.builtInKotlin=false.Because Flutter's fallback currently masks the bug, the decisive reproduction removes that mask. In a local Flutter SDK checkout, extend
kgpRegexKotlininpackages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.ktso it also matches the imperative form, by prefixing this alternative to the existing pattern:Then, in any of the affected packages' examples, bump the example to AGP 9 and run
flutter build apk --debugwithandroid.builtInKotlin=false. The build fails with theKotlinAndroidProjectExtension does not existerror above. Reverting the SDK edit makes it build again — confirming the plugin currently depends on Flutter's fallback rather than on its own guard.Evidence that the condition is wrong, not just unlucky
android.builtInKotlindefaults to enabled in AGP 9. Fromcom/android/build/gradle/options/BooleanOptioningradle-9.1.0.jar:So: absent ⇒ built-in Kotlin on; explicit
false⇒ off. The version check alone cannot tell those apart. (It is alsoSoftlyEnforcedwith removal targeted at AGP 10, so opting out is temporary.)Proposed fix
Guard on the same condition AGP itself uses — built-in Kotlin is on when AGP >= 9 and
android.builtInKotlinis not explicitlyfalse:The
project.extensions.configure(KotlinAndroidProjectExtension::class.java) { … }block below stays unchanged: AGP 9's built-in Kotlin registers that extension, so it resolves under both branches (confirmed on a real AGP 9.1 build withandroid.builtInKotlin=true).On AGP 8 the behaviour is provably identical to today:
agpMajor >= 9is false, sobuiltInKotlinEnabledis false and KGP is applied exactly as before.Note on prior art
app_settings8.0.3 hit this exact failure and fixed it (spencerccf/app_settings#270); itsandroid/build.gradlecarries a long comment documenting the mechanism. Its fix is not worth copying verbatim: it reads the property with a?: 'false'default and ignores the AGP version, so on an AGP 9 project that never sets the property it applies KGP into a build where built-in Kotlin is already active, which throws:Flutter apps always write the property, so that case is masked there — but the version above is correct in both cases.
Affected packages
android_alarm_manager_plusbattery_plusdevice_info_plusnetwork_info_pluspackage_info_plussensors_plusshare_plusandroid_intent_plususes a Groovybuild.gradlewith no Kotlin sources and is not affected.Verification performed
builtInKotlin=false, before fixKotlinAndroidProjectExtension does not existbuiltInKotlin=false, after fixbuiltInKotlin=trueAdditionally, after the fix, Flutter's warning lists the plugin under "plugins that apply KGP", which means the patched regex did detect the declaration and Flutter did not apply KGP as a fallback — the plugin stands on its own.
As a control, the fix was reverted in a single package (
share_plus) and the AGP 9 build failed identically, confirming the passing results are meaningful rather than vacuous.Per CONTRIBUTING, this is filed as one issue with a proposal, with one branch and pull request per package to follow.