This document is the contributor-facing record of the migration of the Flutter Gradle Plugin (FGP) off the legacy Android Gradle Plugin (AGP) DSL/Variant API and AGP internals, onto the public API surface shipped in the com.android.tools.build:gradle-api artifact.
Umbrella issues:
The user-facing breaking-change page draft lives next to this file in website-page-draft.md. It must be published to docs.flutter.dev/release/breaking-changes/ before the newDsl flip (phase P9) reaches the beta channel.
AGP 9 (January 2026) deprecated the old DSL and Variant APIs behind the android.newDsl=false escape hatch. AGP 10 (late 2026) removes those APIs entirely and removes access to AGP internals — only the public surface of the gradle-api artifact remains. Today the FGP:
com.android.tools.build:gradle artifact (packages/flutter_tools/gradle/build.gradle.kts);applicationVariants, libraryVariants, variant.outputs, assembleProvider, packageApplicationProvider, versionCodeOverride);BaseExtension (FlutterPluginUtils.getLegacyAndroidExtension);com.android.build.gradle.internal.dsl.BuildType in plugins/PluginHandler.kt);com.android.build.gradle.internal.utils.getKotlinAndroidPluginVersion in VersionFetcher.kt);flutter build aar with legacy dynamic Groovy in aar_init_script.gradle.Flutter templates pin AGP 9.1.0 but ship android.newDsl=false, and a tool migrator (disable_new_dsl_migration.dart) adds the opt-out to existing projects. That opt-out dies with AGP 10.
gradle-api.android.newDsl=false.flutter create app builds with newDsl on.gradle-api:8.11.1 (decompiled jar inspection). If implementation finds a replacement API that genuinely requires a higher min AGP: document which API and why no compatible alternative exists in this file, then bump — otherwise version floors are untouched by this work.AgpCommonExtensionWrapper.kt exists precisely because the public CommonExtension broke between AGP 8 and 9. Mitigation: a CI/test axis compiling the FGP against gradle-api 9.x is mandatory from phase P2 onward, plus a bytecode check (javap grep) that no compiled FGP class references CommonExtension as an owner.android.builtInKotlin=false stays out of scope. Flipping it requires the separate built-in-Kotlin migration workstream. Users get a second (smaller) gradle.properties churn later; the breaking-change page states this explicitly. Corollary: the P9 removal migrator must anchor on the android.newDsl property line — never on marker-comment wording alone — because the template's builtInKotlin marker comment is nearly identical.finalizeDsl snapshot. Preferred mechanism (spiked first in P6): read-then-set on VariantOutput.versionCode inside onVariants — it is seeded with the merged value; set abiOffset * 1000 + current, avoiding a self-referential .map. Fall back to a snapshot only if read-then-set is impossible; record the outcome here.VariantOutput.versionCode.orNull read at onVariants time, then set(abiOffset * 1000 + base)); the sandbox could not execute builds, so the split-per-abi × flavor-defined-versionCode apkanalyzer check in CI is the confirming gate. The finalizeDsl snapshot fallback remains unimplemented.onVariants build scripts: Under modern AGP 9 androidComponents.onVariants, plugins applied at line 25 of build.gradle.kts execute their callbacks before app-level onVariants blocks at line 70 (FIFO callback ordering). Therefore, when building --split-per-abi, FGP seeds output.versionCode (abiOffset * 1000 + base) before app-level callbacks run. Any custom app-level onVariants block that reads output.versionCode.get() will observe the offset value rather than the base versionCode. For standard apps, no change is needed. Even if an app script transforms versionCode (e.g. via multiplication or addition), monotonic ABI ordering (armeabi-v7a < arm64-v8a < x86_64) and Google Play Store uniqueness are preserved. If an app requires an exact literal numeric formula on base versionCodes, it can subtract abiOffset before its transformation and re-add it afterwards.@OutputFiles (from target platforms × flavor × build mode) instead of the shared outputs/flutter-apk directory, because a shared @OutputDirectory would overlap between variants by construction. A runtime warning reports produced names outside the predicted set.DisableNewDslMigration, which ADDS android.newDsl=false. Once the P9 removal migrator reaches a channel, switching between that channel and stable would flip-flop gradle.properties. Before P9 is released, cherry-pick a guard onto the stable-side add-migrator (skip adding the opt-out when the project's recorded Flutter/AGP versions indicate the new-DSL-capable plugin) or accept the flip-flop for the overlap window and document it in the release notes.newDsl=true + custom build type, verifying that build-type creation from pluginProject.afterEvaluate still works) could not run in the implementation sandbox (no AGP artifact access). The initWith copy landed on the primary approach; the android_plugin_example_app_build integration test and a custom-build-type scratch build must confirm it in CI. Documented fallback if afterEvaluate mutation is rejected under newDsl: perform the copy in androidComponents.finalizeDsl on the plugin project instead.buildModeFor semantics. Every variant-scope call uses the (name, debuggable) overload with the public Component.debuggable. Name-based inference is confined to the one DSL-scope case with no public signal (the library-plugin build-type copy in PluginHandler). This preserves add-to-app custom-debuggable matching (a host staging debuggable build type maps to debug engine artifacts).| Legacy usage | Where | Public replacement | Phase |
|---|---|---|---|
internal.utils.getKotlinAndroidPluginVersion | VersionFetcher.kt | delete; rely on existing fallback chain (kotlin_version property → KotlinAndroidPluginWrapper.pluginVersion → reflection); null when KGP absent is OK | P1 |
compileSdkVersion string compare ("android-NN" substring) | FlutterPluginUtils.getCompileSdkFromProject, PluginHandler warning | wrapper compileSdk / compileSdkPreview; numeric compare with defined preview semantics | P1 |
BaseExtension.ndkVersion | FlutterPluginUtils.getConfiguredNdkVersion | wrapper ndkVersion | P1 |
buildModeFor(BuildType) (legacy model type) | FlutterPluginUtils.kt | buildModeFor(name, debuggable) overload | P2 |
getLegacyAndroidExtension(project).buildTypes loops | PluginHandler.kt | wrapper new-DSL buildTypes container | P2 |
internal.dsl.BuildType live aliasing into plugin projects | PluginHandler.kt | initWith-based copy on new-DSL BuildType; app-specific props only when both sides are ApplicationBuildType | P3 |
BaseExtension / getLegacyAndroidExtension (remaining call sites) | FlutterPluginUtils.kt | wrapper accessors incl. externalNativeBuild | P4 |
eager applicationVariants.configureEach task creation; mergeAssets/processResources hooks | FlutterPlugin.kt, FlutterPluginUtils.kt | consolidated onVariants block; CopyFlutterAssetsTask + variant.sources.assets.addGeneratedSourceDirectory | P5 |
variant.outputs + packageApplicationProvider + doLast APK copy; versionCodeOverride | FlutterPluginUtils.kt | CopyFlutterApksTask (SingleArtifact.APK + BuiltArtifactsLoader); read-then-set VariantOutput.versionCode | P6 |
libraryVariants.all × host applicationVariants.all cross-wiring | FlutterPlugin.kt (add-to-app) | library-side onVariants with Component.debuggable; no host-project lookup | P7 |
dynamic Groovy legacy API in aar_init_script.gradle | aar_init_script.gradle | components-based enumeration; ext-property guard | P8 |
android.newDsl=false template/migrator | templates, disable_new_dsl_migration.dart | drop from templates; RemoveNewDslOptOutMigration | P9 |
FULL gradle artifact dependency | build.gradle.kts | gradle-api artifact (compile-time proof of zero internal usage) | P10 |
Each phase is one PR-sized change on its own branch. P8 is an independent lane (Groovy script, disjoint files); P0/P1 are disjoint from each other; everything else serializes through FlutterPlugin.kt / FlutterPluginUtils.kt.
| Phase | Branch | Size | Summary |
|---|---|---|---|
| P0 | agp-api-doc | S | this doc + website page draft |
| P1 | agp-internal-utils | S | VersionFetcher internal util removal; numeric compileSdk compare; ndkVersion via wrapper |
| P2 | agp-buildmode-deps | M | buildModeFor overloads; new-DSL flutter dependencies; 9.x compile axis |
| P3 | agp-plugin-buildtypes | M | initWith copy for plugin build types; drop internal import; internal-import lint |
| P4 | agp-ndk-fallback | S | delete BaseExtension; externalNativeBuild via wrapper |
| P5 | agp-assets-onvariants | L | lazy task registration (5a) + generated-asset-dir wiring (5b) |
| P6 | agp-apk-copy-versioncode | L | CopyFlutterApksTask; per-ABI versionCode; app path legacy-free |
| P7 | agp-add-to-app | L | library-side onVariants; delete host cross-wiring + P5a legacy fork |
| P8 | agp-aar-script | M | aar_init_script public-API cleanup |
| P9 | agp-newdsl-flip | M | templates drop opt-out; removal migrator; new error handlers |
| P10 | agp-gradle-api | M | dependency swap to gradle-api; test migration |
gradle_errors.dart matcher and its Dart test in the same PR.CommonExtension bytecode check.com.android.build.gradle.internal.* imports in src/main.| Phase | Revert window |
|---|---|
| P0 | always revert-safe |
| P1–P4 | each until the next phase in the chain lands; then fix-forward |
| P5 | until P6 lands |
| P6 / P7 | mutually tolerant (disjoint app/module paths) until P10 |
| P8 | revert-safe even after P10, but not after P9 |
| P9 | cleanly revertible in isolation |
| P10 | cleanly revertible in isolation |
applicationVariants.all APK-rename recipes) fail under newDsl — the biggest break. Mitigated by new error handlers (P9) and the website page.app[-abi][-flavor]-<mode>.apk, byte-matching the current concatenation order), but an UP-TO-DATE-capable finalizer task replaces the doLast block; new task names appear in gradlew tasks.finalizeDsl user mutations (afterEvaluate CI patterns) may behave differently; a runtime divergence warning is added.initWith copies; library plugins cannot receive isDebuggable (no public setter on LibraryBuildType) — plugin-side BuildConfig.DEBUG/JNI debuggability may differ for custom debuggable build types; matching preserved via matchingFallbacks.:app:merge<V>Assets.dependsOn edge and host-project lookup are removed; flutter.hostAppProjectName becomes a no-op with a deprecation warning naming a removal milestone; ordering against copyFlutterAssets<V> task names may break.TaskProviders, and copyFlutterAssets<V> changes type from org.gradle.api.tasks.Copy to a custom task class — tasks.named(..., Copy::class) casts fail.flutter build aar: the singleVariant dedup guard becomes an ext-property/try-catch with a specified error message; variant enumeration moves from libraryVariants to components — partial user singleVariant declarations surface differently.android.newDsl lines (template marker “This newDsl flag was added by the Flutter template”; migrator marker “This newDsl flag was added automatically by Flutter migrator”), anchored on the property line so the adjacent builtInKotlin lines are never touched; hand-added opt-outs are respected.Full matrix at P6, P7, P9, P10; targeted per-phase otherwise.
cd packages/flutter_tools/gradle && ./gradlew test (+ the R3 9.x axis)integration.shard tests named in each phase--flavor; --split-per-abi (+ apkanalyzer versionCode assertions, including the flavor-defined-versionCode case); --deferred-components; plugin with a custom build type; flutter build aar; add-to-app source & AAR host flows; flutter run / hot restart / flutter attach; Windows smoke for the copy tasksnewDsl=false; staged newDsl=true per R6