fluttercommunity / fluttercommunity/plus_plugins
Android plugins apply KGP based on AGP major version only, breaking on AGP 9 with android.builtInKotlin=false
- Lingua principale
- Dart
- Stelle
- 1.9k
- Fork
- 1.3k
- Metriche di merge delle PR
- Nessuna PR unita negli ultimi 30g
Descrizione
## Summary
Seven Android plugins decide whether to apply the Kotlin Gradle Plugin (KGP) based on the AGP **major version alone**:
```kotlin
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]
```
## 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:
```kotlin
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`:
```kotlin
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.
Guida per i contributori
Apri la guida per i contributori
Direzione di ricerca
Start with the Android build.gradle.kts files in android_alarm_manager_plus, battery_plus, device_info_plus, network_info_plus, package_info_plus, sensors_plus, and share_plus, and compare their AGP/KGP guards. Reproduce with AGP 9, android.builtInKotlin=false, and the patched Flutter regex. Done means all seven APK builds pass with the opt-out, built-in Kotlin enabled, and the current AGP 8.12.1 pin.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- android, kotlin
- Ambito
- build-system, mobile
- Tipo di issue
- Bug
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Stato di attività
- Attiva
- Chiarezza
- Specificata chiaramente
- Idoneità per principianti
- 68/100