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.master (PRs #176858 and #177416 bumped the floor to 8.11.1). This migration builds seamlessly on that new floor. Every replacement API used here was verified public in 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.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). Warning: If an app uses a custom build type (like staging), the plugin's BuildConfig.DEBUG and native (C++/JNI) code may silently compile in release mode instead of debug mode. Matching is preserved via matchingFallbacks, but C++ debugging will be broken for those custom build types.: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. Build scripts that reference Flutter's copyFlutterAssets<V> tasks by name (e.g. tasks.getByPath(...)) will crash with a Task with path ... not found error because the tasks are now registered lazily.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