Skip to content

[AGP 9.1.0 Migration #7] Migrate the flutter-apk copy and per-ABI versionCode to the variant API - #193693

Merged
auto-submit[bot] merged 4 commits into
flutter:masterfrom
reidbaker-agent:agp-apk-copy-versioncode
Oct 8, 2026
Merged

auto-submit[bot] merged 4 commits into
flutter:masterfrom
reidbaker-agent:agp-apk-copy-versioncode

Conversation

@reidbaker-agent

@reidbaker-agent reidbaker-agent commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Description

This is PR 7 of 11 in the AGP 9.1.0 / public gradle-api migration stack (#180137, #166550).

This pr changes the way splitting by api version codes work. This can be a surprising change but I have not found a work around.

PR 8 adds add-to-app support and as a result deletes a bunch of code that exists to bridge the partial migration. See #19371 specifically https://github.com/flutter/flutter/pull/193717/changes/f668780fd47caca13c03828f49d21572074825d6..a6be16034c8a1a4a0456cd35194b5973cfd4dc75#r4185414230 which is a subset of changes.

Agent authored details

Changes

  1. flutter-apk copy on the variant API. CopyFlutterApksTask (copyFlutterApks<Variant>) copies the variant's SingleArtifact.APK outputs, read through BuiltArtifactsLoader, into build/app/outputs/flutter-apk/. The names are app[-abi][-flavor]-<mode>.apk, the format listApkPaths in lib/src/android/gradle.dart expects. assemble<Variant> depends on the task.

    • The task has no Project or variant fields. It uses injected FileSystemOperations and @InputDirectory @PathSensitive(RELATIVE).
    • It is annotated @DisableCachingByDefault(because = "Not worth caching"), the annotation and reason Gradle uses on its own Copy and Sync tasks.
    • Its declared outputs are the individual APK files (@OutputFiles). The shared flutter-apk directory is not declared, because it would overlap between variants.
    • One naming function, CopyFlutterApksTask.apkFileName, produces both the declared outputs and the copied files. Its inputs are the ABIs of the variant's outputs, the flavor, and the build mode.
  2. Per-ABI versionCode without reading output.versionCode. For --split-per-abi, the plugin sets output.versionCode = ABI_VERSION[abi] * 1000 + base on each ApplicationVariant output with an ABI filter. It never reads the property, because AGP rejects that read during configuration when android.compatibility.enableLegacyApi=false.

    • DslVersionCodes takes a snapshot of defaultConfig.versionCode and each product flavor's versionCode in androidComponents.finalizeDsl.
    • forVariant(variant.productFlavors) resolves the base the way AGP merges it: the first flavor, in dimension order, that sets a versionCode, otherwise defaultConfig.
    • If the DSL declares no versionCode, Flutter logs a warning and leaves the variant's versionCodes unchanged.
  3. APK transform errors point at the transform. The copy fails in two cases. Both can happen only if another plugin or build script transforms SingleArtifact.APK:

    • the APK directory has no output-metadata.json;
    • an APK has an ABI filter that none of the variant's outputs declares.

    Both error messages name SingleArtifact.APK and tell the user to contact the maintainer of the plugin that does the transform. They do not ask for a Flutter issue. The task KDoc and website-page-draft.md say what a transform must keep. Plugins Flutter developers commonly use (Walle, VasDolly, packer-ng, AndResGuard, Sentry, Firebase App Distribution, Gradle Play Publisher) do not trigger either error; the evidence is in this review reply.

  4. Deleted the app-path applicationVariants.configureEach block and configureAbiVersionCodeOverride, together with their two @Suppress("DEPRECATION") markers and the unsafe as ApkVariantOutput cast. The add-to-app module path (addFlutterDepsForModule, libraryVariants) is left for PR 8.

  5. Structure follows the PR 6 review (@mboetger's point that registerFlutterAssetTasks should not register the compile task). onVariants calls configureSplitPerAbiVersionCodes and registerCopyFlutterApksTask after registerFlutterAssetTasks, under the same isApplicationProject && shouldCompileFlutterForVariant gate. Each helper does one job, and no helper takes a parameter only to pass it on.

  6. Docs:

    • Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md: decision record 4, the replacement map, and items 2 and 3 of "Features that must break".
    • website-page-draft.md: versionCode recipes that do not read output.versionCode, and the rules for APK transforms.
    • Links to the draft carry a TODO for #193713, which tracks publishing it on docs.flutter.dev.
  7. Tests:

    • CopyFlutterApksTaskTest (new, 5 tests).
    • DslVersionCodesTest (new, 5 tests).
    • 8 new FlutterPluginTest cases: defaultConfig and flavor offsets, the no-versionCode warning, no split, the force flag, copy wiring, profile/custom build type naming, and assemble wiring. The mocked VariantOutput.versionCode stubs only set, so any read fails the test.
    • flutter_build_apk_split_per_abi_test.dart: a fourth test builds with android.compatibility.enableLegacyApi=false, and every test asserts each per-ABI APK is in flutter-apk.

Review response (commit 33f39876b92)

Comment Change
Caching annotation: "How do I know this is true?" Uses Gradle's own Copy/Sync annotation and reason (checked with javap).
Why would load return null? / How would ABIs mismatch? Comments say exactly when each can happen. Errors point at the plugin that transforms the APK.
Other places that build APK names FGP has one. The tool's three reading sites are listed in the reply (ratchet: not changed here).
Custom variants, and proof the copy works Build-mode rule matches the deleted code. Results of a flavor and custom build type scratch app are in the reply.
Temporal wording Removed from KDoc and docs.
Link to the website docs TODO tied to #193713 until the page is published.
Reading output.versionCode depends on compatibility mode No read. finalizeDsl snapshot instead, with a compatibility-mode-off integration test.
Why ×10000, and can the earlier numbers be kept? Explained in the replies. Keeping them requires reading the value the app set, which AGP rejects with compatibility mode off.

Why removing the deleted code is safe

The deleted code used AbstractAppExtension.applicationVariants, ApkVariantOutput.versionCodeOverride/getFilter, and packageApplicationProvider. AGP deprecated these, and they are not available with AGP 9's android.newDsl=true. The replacements (ApplicationVariant.outputs, VariantOutput.versionCode/filters, SingleArtifact.APK, BuiltArtifactsLoader, DslLifecycle.finalizeDsl) are public com.android.build.api APIs available at the stack's AGP 8.11.1 floor.

Behavioral and compatibility notes

  1. versionCode for apps that transform it in their own onVariants (breaking). Flutter registers its onVariants callback when the plugin is applied, so AGP runs it before an app's androidComponents.onVariants block. versionCodeOverride was applied after that block. Apps that set versionCode only in the android {} DSL see no change. An app that multiplies by 10000 (compatibility mode on), built with --build-number 42:

    ABI (offset) versionCodeOverride This PR
    armeabi-v7a (1) 421000 10420000
    arm64-v8a (2) 422000 20420000
    x86_64 (4) 424000 40420000

    Values stay distinct, keep ABI order, and only go up, so Play upgrades keep working. With compatibility mode off, apps can't read output.versionCode either. For that case the docs give a recipe that sets an absolute value in the app's onVariants, which replaces Flutter's: offset * 1000 + flutter.versionCode * 10000.

  2. A versionCode declared only in the manifest is not offset. The DSL snapshot can't see it, so Flutter logs a warning and leaves the variant's versionCodes unchanged.

  3. A finalizeDsl callback registered after Flutter's that changes versionCode is not seen. This is documented in the DslVersionCodes KDoc and the migration doc.

  4. copyFlutterApks<Variant> tasks appear in gradlew tasks and can be UP-TO-DATE.

  5. Two build types that map to the same Flutter mode share a file name. For example, debug and a debuggable staging both write app-debug.apk; the deleted code had the same clash. Running both assembles in one ./gradlew invocation makes the two copy tasks alternate. The flutter tool builds one variant per invocation, so it is unaffected.

Integration test coverage of the changed paths

Path Tests
Per-ABI versionCode: default, build number, app onVariants transform, compatibility mode off flutter_build_apk_split_per_abi_test.dart (4 tests; checks versionCodes with apkanalyzer and the per-ABI files in flutter-apk)
flutter-apk copy, default and flavored android_gradle_asset_merging_test.dart, gradle_libapp_so_packaging_test.dart (incl. flavors), android_plugin_example_app_build_test.dart, isolated/native_assets_flutter_build_test.dart; devicelab flavors_test, gradle_desugar_classes_test
Plugin unit tests on AGP 8.11.1 and AGP 9 android_run_flutter_gradle_plugin_tests_test.dart

Tests run locally

  • ./gradlew test in packages/flutter_tools/gradle (JDK 17): pass (33 suites, 0 failures).
  • ktlint 1.5.0 with the CI .editorconfig and baseline: clean.
  • dart format / dart analyze --fatal-infos on the changed Dart test: clean.
  • flutter_build_apk_split_per_abi_test on 33f39876b92: 4/4 pass.
  • On the review changes before the final edits to comments and error messages: android_gradle_asset_merging_test (4), gradle_libapp_so_packaging_test (4), android_run_flutter_gradle_plugin_tests_test (2) all pass.
  • Manual build with enableLegacyApi=false (AGP 9.3.1): versionCodes 1042/2042/4042. A negative control that re-adds the read fails with "configuration of project ':app' has not completed yet".
  • Scratch app with free/paid flavors and a custom staging build type: APK names and versionCodes match the deleted code.

Pre-launch Checklist

@github-actions github-actions Bot added platform-android Android applications specifically tool Affects the "flutter" command-line tool. See also t: labels. d: docs/ flutter/flutter/docs, for contributors labels Oct 2, 2026
The application path of the Flutter Gradle Plugin no longer uses the
deprecated BaseVariant API (applicationVariants / ApkVariantOutput):

- CopyFlutterApksTask copies the variant's SingleArtifact.APK outputs,
  read through BuiltArtifactsLoader, into build/outputs/flutter-apk under
  the unchanged names app[-abi][-flavor]-<mode>.apk. assemble<Variant>
  depends on it. The task has no Project or variant fields and uses an
  injected FileSystemOperations.
- A single naming function in the task produces both the declared
  @OutputFiles and the copied file names. Its inputs are the ABIs of the
  variant's outputs (what AGP builds), the flavor, and the build mode.
  The task fails if AGP built an APK for an ABI that was not declared.
- Per-ABI versionCode for --split-per-abi is a read-then-set on
  VariantOutput.versionCode in onVariants. This is a documented
  exception to the no-configuration-time-get rule, because the lazy
  form is circular. The read relies on AGP's compatibility mode
  (android.compatibility.enableLegacyApi, on by default through 9.x).
- The applicationVariants.configureEach block and its
  @Suppress("DEPRECATION") markers are deleted. The add-to-app module
  path keeps using libraryVariants until the next phase.

Behavior change: Flutter's onVariants callback runs before an app's own
androidComponents.onVariants block, while ApkVariantOutput's
versionCodeOverride was applied after it. Apps that transform
output.versionCode there transform Flutter's offset value (for example
arm64-v8a, build 42, x10000: 422000 -> 20420000). The migration doc and
the breaking-change page draft describe this and give a recipe that
keeps the 422000-style numbers. No divergence warning is emitted,
because it would fire on the onVariants pattern that AGP recommends.

Part of flutter#166550.
@reidbaker
reidbaker force-pushed the agp-apk-copy-versioncode branch from 79ef973 to e15544b Compare October 2, 2026 01:38
@reidbaker reidbaker added the CICD Run CI/CD label Oct 2, 2026

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.

Reviewers. This document feels AI generated but I have left it alone since it is not code and might help you review (things like the table for version codes).

* [outputApks] instead. Those and the files the copy writes are both named by [apkFileName] from
* the same inputs, so the declared outputs cannot drift from what the task produces.
*/
@DisableCachingByDefault(because = "Copying local APKs is faster than unpacking cache archives")

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.

How do I know this is true?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You can't. It was an unmeasured claim, so I replaced it. The annotation now reads
@DisableCachingByDefault(because = "Not worth caching"). That is the same annotation and reason
Gradle puts on its own Copy and Sync tasks (checked with javap -v on
gradle-core-8.9.jar: org.gradle.api.tasks.Copy and Sync both carry
DisableCachingByDefault(because="Not worth caching")). The KDoc says that and makes no
performance claim.

Comment on lines +93 to +96
"Flutter could not read the APK metadata in $apkDir, so it cannot copy the " +
"APKs to ${destinationDir.get()}. Please file an issue at " +
"https://github.com/flutter/flutter/issues."
)

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.

What are some reasons that we would fail to be able to load the apk. I can think of 1) when our name building produces a name that does not match gradle 2) possibly a file permission issue. Anything else?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

BuiltArtifactsLoader.load(dir) returns null in exactly one case: dir/output-metadata.json
does not exist (AGP BuiltArtifactsLoaderImpl.loadFromFile: if (inputFile == null || !inputFile.exists()) return null).

  • Name building: not involved. load runs before any name is computed, and the names come
    from task inputs, not from files on disk.
  • File permissions: an unreadable file makes readText() throw an IOException, and
    malformed JSON throws too. Neither returns null.
  • Practical cause: AGP's packaging task always writes the metadata file. A transform that
    uses toTransformMany(SingleArtifact.APK) with ArtifactTransformationRequest gets it written
    by AGP ("this object will abstract away having to deal with [BuiltArtifacts] and manually load
    and write the metadata files", ArtifactTransformationRequest KDoc). So null means another
    plugin or build script replaced the artifact with plain toTransform and did not call
    BuiltArtifacts.save().

I put this in a comment above the call. The error says "no output-metadata.json", names
SingleArtifact.APK, and asks the user to contact the maintainer of the plugin that transforms
it. It does not ask them to file a Flutter issue. CopyFlutterApksTaskTest asserts both of those.

Comment on lines +101 to +103
// means AGP built an APK that Flutter did not declare as an output. Fail rather than
// write a file that Gradle does not track.
if (abi !in declaredAbis) {

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.

How would these get mismatched?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

With AGP alone they can't. AGP builds one APK per variant output, and the declared ABIs come from
those same outputs (a disabled output only makes the built set smaller, and that passes). The
check can fail only if another plugin or build script transforms SingleArtifact.APK into APKs
with different ABI filters. I kept it, because without it such a build writes an untracked file
into flutter-apk and up-to-date checks go wrong silently. The behavior is documented in three
places:

  • The CopyFlutterApksTask class KDoc.
  • The error message. It names SingleArtifact.APK and points at the plugin that transforms it,
    not at Flutter.
  • website-page-draft.md, which says what a transform must keep.

I also looked for plugins Flutter developers use that touch the APK:

Plugin What it does to the APK Trips the check?
Walle (Meituan), used by Flutter devs through pub package_by_walle and in #28701 writes extra channel APKs after assemble (legacy variant.outputs) No
VasDolly (Tencent) reads SingleArtifact.APK with BuiltArtifactsLoader, writes to its own dir No
packer-ng (archived) writes extra APKs after assemble No
AndResGuard (Tencent), Redex overwrite the APK in place after assemble No (metadata and filters unchanged)
Sentry, Firebase App Distribution, Gradle Play Publisher read SingleArtifact.APK only No
toTransformMany(SingleArtifact.APK) + ArtifactTransformationRequest (10 sampled build scripts, mostly renaming or signing) AGP writes the metadata No

I found no public plugin that uses plain toTransform(SingleArtifact.APK) without writing the
metadata, or that changes ABI filters. DexGuard, Crashlytics, AGConnect and the 360/Legu
hardening tools are closed source, so I couldn't verify them.

* `packages/flutter_tools/lib/src/android/gradle.dart`, which is how the Flutter tool
* finds these files.
*/
internal fun apkFileName(

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.

Is this the only place we build apk files names based on gradle parameters?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In the Gradle plugin, yes. This PR deletes the other one, the filename += block in the
applicationVariants callback. The Flutter tool rebuilds the names on the reading side in three
places:

Where Used for Name format
CopyFlutterApksTask.apkFileName (FGP) writes flutter-apk app[-abi][-flavor]-<mode>.apk
listApkPaths, lib/src/android/gradle.dart:1278 flutter build apk for app projects same
AndroidApk.fromAndroidProject, lib/src/android/application_package.dart:118-125 flutter run/install (no ABI) app[-flavor]-<mode>.apk
_apkFilesFor, lib/src/android/gradle.dart:149 add-to-app host (host/outputs/apk) app[-flavor]-<abi>-<mode>.apk, which is AGP's own naming, not FGP's

The FGP and tool sides are kept in sync only by tests on each side: CopyFlutterApksTaskTest,
the tool's gradle_test.dart, and integration tests that read flutter-apk. Nothing ties the two
together. Following the ratchet principle, I'm raising this here rather than changing it in this
PR.

Comment on lines +738 to +739
* seeds `output.versionCode` (with -1 when no versionCode is declared anywhere, as
* `BaseVariant.getVersionCode` did), so the check for an unset value is only a guard.

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.

Temporal words describing previous behavior I thought were banned by the style guide?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 33f3987. The KDoc describes only the current behavior. The history (versionCodeOverride
ordering, manifest fallback) is in item 3, "Per-ABI versionCode", under "Features that must break" in
the migration doc.

* `ApkVariantOutput.versionCodeOverride`, which this replaces, was applied after such
* blocks, so for those apps the resulting versionCode differs from earlier Flutter
* releases. See "Features that must break" in
* docs/platforms/android/Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md.

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.

I would prefer linking to the website documentation if possible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The breaking-change page isn't published yet. Its source is
docs/platforms/android/website-page-draft.md, so the KDoc points at that section ("Setting
per-ABI or per-variant versionCode") and has a TODO(reidbaker) to switch to the docs.flutter.dev
URL, tracked in #193713. The migration doc links the
same issue.

Comment on lines +767 to +768
// The read relies on AGP's compatibility mode (`android.compatibility.enableLegacyApi`,
// on by default through AGP 9.x). With it off, AGP disallows unsafe reads of this

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.

This is probably bad given that the goal is to no longer depend on the legacy api.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, and fixed. The plugin no longer reads output.versionCode:

  • DslVersionCodes snapshots defaultConfig.versionCode and each product flavor's
    versionCode in androidComponents.finalizeDsl.
  • forVariant(variant.productFlavors) resolves the base the way AGP merges it: the first flavor
    in dimension order that sets one, then defaultConfig.
  • onVariants only calls output.versionCode.set(...).

Evidence:

  • -Pandroid.compatibility.enableLegacyApi=false on AGP 9.3.1 builds and gives 1042/2042/4042.
  • Re-adding the .orNull read fails with "Cannot query the value of this property because
    configuration of project ':app' has not completed yet".
  • A strict-mode case was added to flutter_build_apk_split_per_abi_test.
  • The unit-test Property mock stubs only set, so any read fails the test.

Trade-offs, now documented:

  • A versionCode declared only in the manifest is not offset. Flutter logs a warning in that case.
  • A finalizeDsl callback that runs after Flutter's and changes versionCode is not seen.


// Check that `flutter build apk --split-per-abi` generates a versionCode equal to abiIndex * 1000 + buildNumber
// Check that `flutter build apk --split-per-abi` generates a versionCode equal to
// abiIndex * 1000 + buildNumber (then multiplied by 10000 when [usingCustomAppGradleFile]), and

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.

Why multiplied by 10000?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It comes from #174081 (gmackall), the regression test for #173917. In that issue an F-Droid user
needs versionCode * 10000 + abiCode * 1000 and set it with
output.versionCode.set(output.versionCode.get() * 10000 ...) in their own onVariants. The
fixture copies that transform. Its abiCodes map is left over from the issue snippet and is
only used as a non-null guard. The test checks that an app's onVariants change to versionCode
survives Flutter's per-ABI offset.

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.

Is there any way to keep the this behavior while migrating to the new api?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not without depending on the compatibility mode. The legacy result
(offset * 1000 + <the value the app's onVariants set>) needs Flutter to run after the app's
callback and read the value the app set. That read is exactly what AGP rejects when
enableLegacyApi=false. Gradle Property has no lazy "transform my current value" (the .map
form is circular), so no lazy workaround exists.

  • Possible but not recommended: register Flutter's onVariants from inside its
    finalizeDsl, which runs after the app's script has registered its callbacks, and read the
    app's value only when compatibility mode is on. I haven't tested this. It keeps the old
    numbers only on AGP 9 with the default setting. In strict mode Flutter would then run last and
    overwrite absolute values set by the app, which is worse.
  • Why the order in this PR fits strict mode: with compatibility mode off, apps can't read
    output.versionCode either, so they have to set absolute values. Flutter running first and the
    app's value winning is what lets that work.
  • Covering [Android] Flutter 3.35.1 cannot customize the version code during compilation. #173917's user: -Pforce-version-code-ignoring-abi=true (Implement version code overrides for split ABIs #187742) plus
    output.versionCode.set(flutter.versionCode * 10000 + abiCode * 1000) gives exactly their
    format, in both modes.
  • Docs: the migration doc and the website draft give the recipe and the before/after table.

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.

@gmackall I rejected the idea of using code that kept the old behavior when we could get the older apis and only do the version breaking on new AGP versions. I just thought that would be confusing for users.

If AGP 10 ends up allowing the opt out to keep working I am going to be sad that we didnt build a way to keep the old behavior. What do you think?

…ures

- Take the per-ABI versionCode base from a finalizeDsl snapshot of the
  DSL versionCodes (DslVersionCodes) instead of reading
  output.versionCode, which AGP rejects during configuration when
  android.compatibility.enableLegacyApi=false. Warn and skip when the
  DSL declares no versionCode.
- Add a strict-mode (enableLegacyApi=false) case to
  flutter_build_apk_split_per_abi_test.
- CopyFlutterApksTask: use Gradle's Copy/Sync caching annotation and
  reason; explain when the metadata can be missing and when the ABI
  check can fail; point both errors at the plugin that transforms
  SingleArtifact.APK instead of asking for a Flutter issue.
- Document what an APK transform must keep in website-page-draft.md.
- Link the website-page-draft.md references to tracking issue flutter#193713.
- Remove temporal wording from KDoc and docs.
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 2, 2026

@reidbaker reidbaker 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.

meta comment. This pr seems to add quite verbose code comments. I have not reviewed all of them but make sure the code comments add value to future maintainers and are easy to understand or make the code easier to understand.

* at Flutter. Plugins that only read the APK artifact, or that write extra APKs after `assemble`
* (as channel-packaging plugins do), do not cause either failure.
*/
@DisableCachingByDefault(because = "Not worth caching")

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.

This reason is much worse than the reason you gave previously.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. "Not worth caching" said nothing. The reason now states two facts you can check: the inputs are APKs already on local disk, and a build-cache entry for this task would store those same bytes again (Gradle caches a task's outputs, and the outputs here are the copied APKs). It makes no speed claim. Fixed in f668780.

// or build script transforms the APK artifact into APKs with different ABI filters.
// Fail rather than write a file that Gradle does not track.
if (abi !in declaredAbis) {
throw GradleException(

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.

duplicate error string? Consider pulling out into a variable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in f668780. Both errors use transformedApkError(problem, fix), so the shared guidance is written once.

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.

@gmackall I rejected the idea of using code that kept the old behavior when we could get the older apis and only do the version breaking on new AGP versions. I just thought that would be confusing for users.

If AGP 10 ends up allowing the opt out to keep working I am going to be sad that we didnt build a way to keep the old behavior. What do you think?

…er comments

- CopyFlutterApksTask: state why caching is disabled (local copy; a cache
  entry would duplicate the APKs) and build both errors through one
  transformedApkError helper.
- Trim comments and KDoc added by this PR to what the code does not
  already say.
@reidbaker-agent

Copy link
Copy Markdown
Contributor Author

Re the review note on verbose comments: thanks. I went through every comment this PR adds and kept only what a maintainer can't get from the code: why the snapshot exists (strict mode), the one case it misses (later finalizeDsl), why the assemble task is matched by name, and where the file-name contract lives. Net −105 comment lines in f668780. The migration doc and website draft were not changed in this pass.

@reidbaker reidbaker added the CICD Run CI/CD label Oct 2, 2026
reidbaker
reidbaker previously approved these changes Oct 2, 2026
@reidbaker
reidbaker marked this pull request as ready for review October 2, 2026 19:53
@reidbaker
reidbaker requested a review from a team as a code owner October 2, 2026 19:53
@reidbaker
reidbaker requested review from camsim99, gmackall and mboetger and removed request for a team October 2, 2026 19:53

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request migrates the Flutter Gradle Plugin's APK copying and per-ABI versionCode configuration to the public Android Gradle Plugin (AGP) variant APIs. It introduces a new CopyFlutterApksTask to copy and rename APKs using SingleArtifact.APK and BuiltArtifactsLoader, replacing the previous doLast copy block. It also adds DslVersionCodes to capture version codes in finalizeDsl and apply per-ABI offsets in onVariants, enabling split-per-abi builds to function when AGP's compatibility mode is disabled. Corresponding unit and integration tests, as well as migration documentation, are updated. Review feedback suggests using tasks.matching instead of tasks.configureEach when configuring the assemble task dependency to avoid configuration performance overhead.

return
}
if (baseVersionCode == null) {
project.logger.warn(

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.

Should this be an error or a warn? The default

} else {
options.add('-q');
}
this will not be shown to the user. Is that ok?

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.

It should be shown to users, good catch. I also do not think it should break the build. I will update this to use the logger we use for user facing errors.

The flutter tool runs Gradle with -q, which hides logger.warn. Log the
warning with logger.error, as the plugin's other user-facing warnings
do, and state the consequence (Google Play needs a distinct versionCode
per APK) and the fix.
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 7, 2026
@reidbaker reidbaker added the CICD Run CI/CD label Oct 7, 2026
@auto-submit
auto-submit Bot added this pull request to the merge queue Oct 8, 2026
@reidbaker reidbaker added the c: API break Backwards-incompatible API changes label Oct 8, 2026
Merged via the queue into flutter:master with commit ecd1ab0 Oct 8, 2026
35 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Oct 8, 2026
EricApostal pushed a commit to EricApostal/flutter that referenced this pull request Oct 12, 2026
… variant API (flutter#193717)

PR 7 ([flutter#193693](flutter#193693)) merged
to `master` as `ecd1ab06865`, and `23ddd6b74e6` merges `master` into
this branch, so the [Files
changed](https://github.com/flutter/flutter/pull/193717/files) tab shows
only PR 8's changes.

PR 8 commits:
- `8c9a54d8297` Unify add-to-app module wiring on the variant API
- `2c28d8f8245` Merge PR 7 head `f668780fd47` (conflicts resolved to PR
7's wording where PR 8 does not change the code)
- `a6be16034c8` Trim comments added by PR 8
- `b8cd39bcf81` Address review: clarify the variant gate and task
registration comments
- `2efbd345505` Address review: check the add-to-app module in
module_host_with_custom_build_test
- `d8f869ec468` Address review: share task registration stubs in
FlutterPluginTest
- `798ac295725` Address review: take ApplicationVariant in
configureApplicationOutputs
- `1366874099e` Address review: drop
android.compatibility.enableLegacyApi from the qa section
- `eb20bbd43de` Merge PR 7 head `25550b3fc49` (no conflicts)
- `23ddd6b74e6` Merge `master` at `ecd1ab06865` (PR 7 squash-merged).
`FlutterPlugin.kt`, `FlutterPluginUtils.kt` and `FlutterPluginTest.kt`
conflicted only because the squash commit is not an ancestor; `master`'s
copies are identical to `25550b3fc49`, already merged by `eb20bbd43de`,
so this branch's side was kept.

## Description

This is PR 8 of 11 in the AGP 9.1.0 / public `gradle-api` migration
stack ([flutter#180137](flutter#180137),
[flutter#166550](flutter#166550)).

An add-to-app module (an Android library) is wired through the same
`androidComponents.onVariants` path as an app. The module adds Flutter's
assets and native libraries to its own variants, and the host app
consumes them like any Android library. Flutter does not look up or
configure the host project. With this PR, a host app with AGP 9's
`android.newDsl=true` builds a Flutter module from source; on
`33f39876b92` it fails with `Check failed.` while configuring `:app`.

**Breaking:** `flutter.hostAppProjectName` has no effect. Setting it
logs a warning.

**Base:** `master`. PR 7 (flutter#193693) merged as `ecd1ab06865`.

<details>
<summary>Agent authored details</summary>

### Changes

1. **Module variants on `onVariants`** (`FlutterPlugin.kt`
`addFlutterTasks`). For every variant, app or library, `onVariants`
calls `registerFlutterCompileTask`, `registerFlutterAssetTasks` and
`registerFlutterJniLibsTask`. Only application variants then call the
new `configureApplicationOutputs` (per-ABI versionCodes and the
flutter-apk copy), which takes an `ApplicationVariant`. There is no
mid-lambda `return@onVariants`.
2. **Every module variant is configured.**
`shouldCompileFlutterForVariant` returns true for a `LibraryVariant`.
The command-line check (`shouldConfigureFlutterTask`, flutter#109560) compares
the command line with the project's own `assemble<Variant>` names, and a
host task such as `:app:assembleDemoStaging` never matches the module
variant it consumes (`debug`, via `matchingFallbacks`). A module has
three variants: the template's `debug` and `release`, plus the `profile`
build type the plugin creates. It declares no flavors
(`templates/module/android/library_new_embedding/Flutter.tmpl/build.gradle.tmpl`).
The tasks are registered lazily, so a build runs only the module variant
the host consumes. `--dry-run` on a host with flavor `demo` and build
types `staging` (falls back to `debug`) and `prod` (falls back to
`release`):

   | Host task | Module Flutter tasks in the task graph |
   | --- | --- |
| `app:assembleDemoDebug`, `app:assembleDemoStaging` |
`compileFlutterBuildDebug`, `copyFlutterAssetsDebug`,
`copyJniLibsflutterBuildDebug` |
| `app:assembleDemoRelease`, `app:assembleDemoProd` |
`compileFlutterBuildRelease`, `copyFlutterAssetsRelease`,
`copyJniLibsflutterBuildRelease` |

3. **Build mode from the module variant.** `registerFlutterCompileTask`
reads the library variant's `buildType` and public
`Component.debuggable`, so the mode follows the module variant the host
consumes.
4. **jniLibs wired from the compile task provider.**
`registerFlutterJniLibsTask` takes `compileTaskProvider` and sets
`intermediateDir` from `compileTaskProvider.map { it.outputDirectory }`.
The `tasks.matching { it.name == … }` dependency and the `findByName`
provider are gone. Both copy tasks are registered only together with
their compile task, so `intermediateDir` is required (not `@Optional`)
in `CopyFlutterJniLibsTask` and `CopyFlutterAssetsTask`, and their
`isPresent` branches are removed.
5. **`flutter.hostAppProjectName`.** `warnIfHostAppProjectNameIsSet`
reads `project.providers.gradleProperty(…)` (not
`rootProject.hasProperty`) and logs `Warning: The Gradle property
'flutter.hostAppProjectName' has no effect. Remove it from
gradle.properties.` The `flutter.hostAppProjectName=SampleApp` line is
removed from
`dev/integration_tests/pure_android_host_apps/android_custom_host_app/gradle.properties`,
so devicelab `module_custom_host_app_name_test` (host project
`:SampleApp`) checks that a host not named `app` builds without the
property.
6. **Deleted:**
- From `FlutterPlugin.kt`: the host-project lookup, the
`appProject.afterEvaluate` × `libraryVariants.all` ×
`applicationVariants.all` loop, the `merge<HostVariant>Assets.dependsOn`
edge, `addFlutterDepsForModule` and `addCopyFlutterAssetsDependency`.
- From `FlutterPluginUtils.kt`: `shouldConfigureFlutterTask(Project,
Task)` and `buildModeFor(com.android.builder.model.BuildType)`. Both
lost their last caller.
7. **Docs.**
- `Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md`: decision
record 5 and "Features that must break" item 6 are rewritten.
-
[`website-page-draft.md`](https://github.com/flutter/flutter/blob/master/docs/platforms/android/website-page-draft.md)
(publishing tracked in
[flutter#193713](flutter#193713)): the
add-to-app section is rewritten.
- The text these replace said that the warning names a removal milestone
(it does not), and that a host `staging` debuggable build type gets
debug artifacts (the module variant decides). It also said that
`tasks.getByPath(":flutter:copyFlutterAssetsDebug")` fails with "Task
with path … not found". That is false: in the scratch host below it
returns a `CopyFlutterAssetsTask`.
8. **Tests.**
   - New `FlutterPluginTest` cases:
- jniLibs wiring from the compile task provider, with no name lookups;
- a module variant configured for a single unrelated command-line task,
with no APK wiring;
     - the module build mode for `debug`, `profile` and `release`;
- the `hostAppProjectName` warning, with no host lookup, and no warning
when it is unset.
   - The skip test also checks that no jniLibs task is registered.
- The new tests share stub helpers (`stubTaskRegistration`,
`captureTaskConfiguration`, `setCommandLineTasks`).
- devicelab `module_host_with_custom_build_test` gets a final section.
The host fixture adds a build type `qa` (debuggable, falls back to
`release`). The section runs `app:assembleDemoQa` with
`android.newDsl=true` and `flutter.hostAppProjectName=app`. It checks
that the output has the warning, and that `app-demo-qa.apk` has the
Flutter assets and `libapp.so` for arm64-v8a and armeabi-v7a, and no
debug assets. The existing sections already check
`app:assembleDemoStaging` (debug artifacts).

### Audit resolution

[PR 8 draft
audit](reidbaker-agent#8 (comment)):

| Item | Resolution |
| --- | --- |
| 1. String-based compile task lookup for jniLibs | Provider wiring,
`FlutterPlugin.kt:549-553`. Unit test `onVariants wires the jniLibs copy
to the output of the variant's compile task`. |
| 2. Mid-block `return@onVariants` | None. `onVariants` (`:329-350`)
calls helpers; app-only wiring is in `configureApplicationOutputs`
(`:647`). |
| 3. Redundant `dependsOn` | None on the assets copy (`:497-530`) or the
jniLibs copy (`:536-560`). |
| 4. Dead overload; `rootProject` read; fixture property |
`shouldConfigureFlutterTask(Project, Task)` deleted.
`providers.gradleProperty` at `:665`. Fixture line removed. |

[PR 7 draft
audit](reidbaker-agent#7 (comment)):
its four items were resolved in flutter#193693. The stale "registered in
`applicationVariants`" KDoc it named has no counterpart here; the
`registerFlutterCompileTask` KDoc names no project type.

### Why removing the deleted code is safe

- The deleted module path used
`com.android.build.gradle.LibraryExtension.libraryVariants`,
`AbstractAppExtension.applicationVariants`,
`BaseVariant.mergeAssetsProvider` and
`BaseVariantOutput.processResourcesProvider`. AGP does not register
those extensions with `android.newDsl=true`, which is why `33f39876b92`
fails with `Check failed.` in the host `afterEvaluate` block that looks
up `LibraryExtension`. Its replacements (`LibraryVariant`,
`Component.debuggable`,
`Sources.assets`/`jniLibs.addGeneratedSourceDirectory`) are public
`com.android.build.api` APIs. FGP compiles against AGP 9.1.1, the
minimum on `master` (flutter#193813), and `Component.getDebuggable` also exists
in gradle-api 8.11.1 (checked with `javap`).
- Remaining `hostAppProjectName` users: none. `git grep
hostAppProjectName` hits only this PR's code, tests and docs, plus an
unrelated iOS `XcodeProject.hostAppProjectName` in the tool. The
templates, `include_flutter.groovy`, `module_plugin_loader.gradle` and
devicelab do not read it. devicelab `module_custom_host_app_name_test`
(host project `:SampleApp`) passes without the fixture property.
- `com.android.builder.model.BuildType` and
`shouldConfigureFlutterTask(Project, Task)` have no remaining
references.

### Removed comments, docstrings and tests

| Removed | Reason |
| --- | --- |
| KDoc and two `TODO(gmackall)` comments (flutter#166550) on
`addFlutterDepsForModule`; KDoc on `addCopyFlutterAssetsDependency` |
Functions deleted. flutter#166550 stays open for the rest of the stack. |
| The "host app build variant → Flutter variant" table comment in the
module block | The mapping is gone; the module variant decides the mode.
|
| KDoc of `shouldConfigureFlutterTask(Project, Task)` and
`buildModeFor(ModelBuildType)` | Functions deleted. |
| `flutterCompileTaskName` KDoc | Nothing references the task by name,
and the one-line name builder needs no doc. |
| flutter#188785 comment in `registerFlutterJniLibsTask`, and the `@Optional`
KDoc and "no Flutter build" comment in `CopyFlutterJniLibsTask` | The
jniLibs task is registered only with its compile task.
`gradle_libapp_so_packaging_test` "app:assembleAndroidTest builds when
no Flutter compile task is configured" still passes. |
| `@Optional` KDoc on `CopyFlutterAssetsTask.intermediateDir` (it kept
`@Optional` for the module migration) | The migration is this PR. |
| `FlutterPluginUtilsTest`: `buildModeFor returns profile if the
BuildType has name profile`, `buildModeFor returns debug if the
BuildType is debuggable`, `buildModeFor returns release if the BuildType
is not debuggable and not named profile` | Overload deleted.
`buildModeFor with a name and debuggable flag prefers the profile name
over debuggability` covers the same cases. |
| `FlutterPluginUtilsTest`: four `shouldConfigureFlutterTask` tests used
a mocked `Task` | Kept with the same names and inputs, calling the
`String` overload. |
| `CopyFlutterAssetsTaskTest`: `clears the destination directory when
there is no flutter build for the variant` | The input is required, so
that case cannot happen. Replaced by `removes staged assets that the
flutter build does not produce`, which keeps the stale-file check. |
| Comments trimmed per review (`a6be16034c8`): the
`PROP_HOST_APP_PROJECT_NAME` KDoc; the `registerFlutterJniLibsTask` KDoc
sentence naming its compile task; the "An application project only has
application variants." comment above the `check`; restatements in the
KDoc of `shouldCompileFlutterForVariant` (PR 7's paragraph restored
verbatim), `registerFlutterCompileTask`, `configureApplicationOutputs`,
`warnIfHostAppProjectNameIsSet` and both `intermediateDir`s; test helper
KDocs and integration test comments; migration doc item 6 | Each
restated the code. What remains is why library variants are ungated, why
the inputs are required, and the test intent. Added code-comment lines
vs `f668780fd47`: 85 → 39. |
|
`packages/flutter_tools/test/integration.shard/android_add_to_app_module_test.dart`
(added by this PR, then deleted per review, `2efbd345505`) | Its `qa`
check moved into devicelab `module_host_with_custom_build_test`, which
builds the same fixture. Its `staging` check duplicated the devicelab
staging section, except for `android.newDsl=true`. The devicelab task
runs presubmit on Linux, Mac and Windows (`.ci.yaml`), like the deleted
test. |
| Rewritten per review (`b8cd39bcf81`): the library paragraph of the
`shouldCompileFlutterForVariant` KDoc, the assets `GradleException`
comment, the `registerFlutterCompileTask` and
`configureApplicationOutputs` KDocs | They explained too little ("What
does this mean?") or restated the code. |
| The `check(variant is ApplicationVariant)` that PR 7 added in the app
`onVariants` path, moved by this PR into `configureApplicationOutputs`
(`798ac295725`) | The call site branches on `variant is
ApplicationVariant`, and the function takes an `ApplicationVariant`, so
the compiler enforces the type. In an application project every variant
is an `ApplicationVariant`, so the APK wiring runs for the same
variants. |
| `-Pandroid.compatibility.enableLegacyApi=false` in the devicelab `qa`
section (`1366874099e`) | AGP 9.1.0, 9.1.1 and 9.3.1 mark the option
`Deprecated(VERSION_10_0)`. Its consumers back the variant API that
`android.newDsl=true` removes. |

### Behavioral and compatibility notes

1. **`flutter.hostAppProjectName` has no effect (breaking).** It logs a
warning and names no removal milestone. Its only use was the host
lookup.
2. **The Flutter build mode follows the module variant the host
consumes.** A host `debug`/`profile`/`release` consumes the module
variant of the same name. A custom host build type gets the module
variant its `matchingFallbacks` select. With a debuggable `qa` that
falls back to `release`, built as the only task, the APKs contain:

   | Build | `flutter_assets` | `libapp.so` | Flutter tasks run |
   | --- | --- | --- | --- |
| `33f39876b92` (`newDsl=false`) | no | no | `compileFlutterBuildDebug`
(not packaged) |
   | This PR | yes | yes | `compileFlutterBuildRelease` |

3. **A host `assemble<Variant>AndroidTest` run on its own runs the
module's `flutter assemble` for that variant.** `--dry-run` of
`:app:assembleDemoDebugAndroidTest` lists
`:flutter:compileFlutterBuildDebug` with this PR, and only
`copyJniLibsflutterBuildDebug` on `33f39876b92`. This is a cost,
accepted because the module cannot map host task names to its own
variants.
4. **Module assets are a generated assets source directory.** The
deleted code copied them into the module's merged-assets output after
`mergeAssets`, and made every build re-run `clean<MergeAssetsTask>`.
Collisions resolve by source-set priority, as for apps since PR 6.
5. **`copyFlutterAssets<V>` in a module is a `CopyFlutterAssetsTask`,
not a `Copy`.** Lookups by name still work; lookups typed as `Copy`
fail.
6. Related open work:
[flutter#191703](flutter#191703) reports an
NPE in `getLegacyAndroidExtension` with `newDsl=true`. That function was
removed earlier in the stack, and this PR removes the next failure on
that path. A module with plugins on `newDsl=true` is not tested here. No
conflict with
[flutter#193682](flutter#193682) or
[flutter#193610](flutter#193610)
(`gradle_errors.dart`); this PR changes no message that the tool
matches. [flutter#184409](flutter#184409)
asks for separate library and app logic in `addFlutterTasks`; this PR
shares `onVariants` and splits only the app outputs.

### Integration test coverage of the changed paths

| Path | Tests |
| --- | --- |
| Module from source, `newDsl=true`, debuggable custom build type on
`release`, `hostAppProjectName` warning | devicelab
`module_host_with_custom_build_test`, `app:assembleDemoQa` section |
| Module from source, flavor and custom build types, debug and release |
devicelab `module_host_with_custom_build_test` (other sections;
`app:assembleDemoStaging` needs the ungated `LibraryVariant`),
`build_android_host_app_with_module_source` |
| Host project not named `app`, without `hostAppProjectName` | devicelab
`module_custom_host_app_name_test` |
| Module AAR (`flutter build aar`) and AAR host | devicelab
`build_aar_module_test`, `build_android_host_app_with_module_aar`;
`android_obfuscate_test`, `android_plugin_new_output_dir_test` |
| App path | `gradle_libapp_so_packaging_test`,
`flutter_build_apk_split_per_abi_test`,
`android_gradle_asset_merging_test`,
`android_run_flutter_gradle_plugin_tests_test` |

### Tests run locally (macOS, JDK 17)

On the head after merging `master` (`23ddd6b74e6`; FGP compiles and
tests against AGP 9.1.1):

- `./gradlew test --rerun-tasks` in `packages/flutter_tools/gradle`: 33
suites, 277 tests, 0 failures.
- ktlint 1.5.0 with the CI `.editorconfig` and baseline: clean.
- `dart format` and `dart analyze --fatal-infos` on
`module_host_with_custom_build_test.dart` and
`gradle_libapp_so_packaging_test.dart`: clean.
- `gradle_libapp_so_packaging_test`: 4 tests pass.
- devicelab, all `success`: `module_host_with_custom_build_test`,
`module_custom_host_app_name_test`,
`build_android_host_app_with_module_source`.

On `1366874099e`:

- `./gradlew test --rerun-tasks` in `packages/flutter_tools/gradle`: 33
suites, 274 tests, 0 failures.
- ktlint 1.5.0 with the CI `.editorconfig` and baseline: clean.
- `dart format` and `dart analyze --fatal-infos` on
`module_host_with_custom_build_test.dart`: clean.
- devicelab `module_host_with_custom_build_test`: `success`. The
`app:assembleDemoQa` section ran `./gradlew app:assembleDemoQa
-Pandroid.newDsl=true -Pflutter.hostAppProjectName=app` and logged the
`flutter.hostAppProjectName` warning.

On `d8f869ec468`:

- `./gradlew test --rerun-tasks` in `packages/flutter_tools/gradle`: 33
suites, 274 tests, 0 failures.
- ktlint 1.5.0 with the CI `.editorconfig` and baseline: clean.
- `dart format` and `dart analyze --fatal-infos` on
`module_host_with_custom_build_test.dart`: clean.
- devicelab `module_host_with_custom_build_test`: `success`. The
`app:assembleDemoQa` section logged the `flutter.hostAppProjectName`
warning.

On `a6be16034c8` (the merge and the trim):

- `./gradlew test` in `packages/flutter_tools/gradle`: 33 suites, 274
tests, 0 failures.
- ktlint 1.5.0 with the CI `.editorconfig` and baseline: clean.
- `dart format` and `dart analyze --fatal-infos` on the changed Dart
files: clean.
- `android_add_to_app_module_test` (deleted in `2efbd345505`): passes.
- devicelab `module_custom_host_app_name_test` (fixture without
`flutter.hostAppProjectName`): `success`, and the log has no
`hostAppProjectName` warning.

On `8c9a54d8297` (before the merge of PR 7's review changes and the
comment trim):

- Integration tests, one at a time, all pass:
`android_add_to_app_module_test` (1), `android_obfuscate_test` (2),
`android_plugin_new_output_dir_test` (1),
`gradle_libapp_so_packaging_test` (4),
`flutter_build_apk_split_per_abi_test` (4),
`android_gradle_asset_merging_test` (4),
`android_run_flutter_gradle_plugin_tests_test` (2).
- `android_add_to_app_module_test` on `33f39876b92`: fails with `Check
failed.`. With `variant is LibraryVariant ||` removed: fails with "does
not contain 'assets/flutter_assets/AssetManifest.bin'".
- devicelab via `bin/test_runner.dart test --exit`, all `success`:
`module_host_with_custom_build_test`,
`module_custom_host_app_name_test`,
`build_android_host_app_with_module_source`, `build_aar_module_test`,
`build_android_host_app_with_module_aar`.
- Scratch module plus the devicelab host fixture (AGP 9.3.1, Gradle
9.5.0):
- Defaults: `assembleDemoStaging` has `kernel_blob.bin` and no
`libapp.so`; `assembleDemoRelease` has `libapp.so` for arm64-v8a,
armeabi-v7a and x86_64.
- Same with `android.compatibility.enableLegacyApi=false`, and with
`android.newDsl=true` added.

</details>

---

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
gmackall pushed a commit to gmackall/flutter that referenced this pull request Oct 20, 2026
…Is (flutter#193962)

PR 7 (flutter#193693) merged to master as `ecd1ab06865` and PR 8 (flutter#193717) as
`4249ccbc18a`. This branch merges master at `4521d8d6634`, so the Files
tab shows only PR 9's changes.

PR 9 commits:
- `86523c549fc` Move aar_init_script to public AGP APIs
- `3edc95d1fa0` Check requested AAR variants in the module and each
plugin
- `da9646aad53` Address review: derive requested variants from
components, register AAR tasks lazily
- `94a29907065` Clarify aar_init_script comments from the adversarial
review
- `ea08280d886` Merge commit '23ddd6b74e6' into agp-aar-init-script (PR
8's head with master at `ecd1ab06865`; no conflicts, no change to PR 9's
files)
- `a17fafcc35e` Merge commit '5ba8961b517' into agp-aar-init-script
(master with PR 8 at `4249ccbc18a`; no conflicts, no change to PR 9's
files)
- `77ba449ea4a` Filter requested AAR components before adding tasks
- `adbfb9cd640` Drop the AGP version list from the duplicate-variant
comment
- `f2b7111c51e` Docs: state the AAR variant requirement in plain words
- `cd10f766b5b` Register an AAR task for every variant, publish only
requested ones
- `b9493299916` Merge commit '4521d8d6634' into agp-aar-init-script
(master, including flutter#193649; no conflicts, no change to PR 9's files)

## Description

This is PR 9 of 11 in the AGP 9.1.0 / public `gradle-api` migration
stack ([flutter#180137](flutter#180137),
[flutter#166550](flutter#166550)).

`flutter build aar` passes
`packages/flutter_tools/gradle/aar_init_script.gradle` to Gradle with
`-I`. The script read `android.libraryVariants` and AGP's internal
`publishing.singleVariants`. With AGP 9's `android.newDsl=true`,
`flutter build aar` on a module fails on `5ba8961b517` with `Can't
generate AAR on a non Android library project.` (script line 13), and
then at line 178 (`Could not get unknown property 'libraryVariants'`).
With this PR the script uses only `pluginManager.hasPlugin`,
`Publishing.singleVariant`, software `components` and `tasks.register`,
and the build succeeds with `newDsl` on and off, on AGP 9.3.1 and on
master's minimum AGP 9.1.1, and on AGP 8.11.1 when FGP's version check
is skipped.

**Breaking:** no. Builds that succeed on `5ba8961b517` succeed with the
same artifacts. Two failing cases change: a module or plugin build file
that declares `singleVariant` for only some variants builds, and a
variant mismatch between the module and a plugin fails with a Flutter
error instead of `Task with name … not found`.

**Base:** `master` at `4521d8d6634`. `aar_init_script.gradle`,
`build_aar_module_test.dart` and both docs are the same at `4521d8d6634`
and `5ba8961b517`, so the comparisons below against `5ba8961b517` hold
for `4521d8d6634`.

<details>
<summary>Agent authored details</summary>

### Changes

All in `aar_init_script.gradle` unless noted. Line numbers are at
`b9493299916`.

1. **Library check** (`:13`):
`project.pluginManager.hasPlugin("com.android.library")` replaces
`android.hasProperty("libraryVariants")`. The error text is unchanged.
2. **Publishing declarations** (`afterProject`, `:185-219`): Flutter
calls `android.publishing.singleVariant(<variant>) { withSourcesJar();
withJavadocJar() }` for each variant, one variant at a time. If the
project's build file already declared that variant, AGP throws, and
`isDuplicateSingleVariantError` (`:125-134`) matches AGP's exact
message, so the build file's declaration is kept. Its comment links the
AGP source of the message ([`AbstractPublishing.kt` at
`studio-2025.3.2`, AGP
9.1.0](https://android.googlesource.com/platform/tools/base/+/refs/tags/studio-2025.3.2/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dsl/AbstractPublishing.kt#38)).
Any other exception is rethrown (`:201-203`). The guard that read
`publishing.singleVariants` and skipped every Flutter declaration once
the build file declared any variant is deleted.
3. **No module-to-plugin `dependsOn`** (`projectsEvaluated`): the loop
over `moduleProject.android.libraryVariants` that made each module
`assembleAar<Variant>` depend on the plugin's task (with eager
`tasks.named(...).get()`) is deleted. The tool requests
`assembleAar<Variant>` without a project path (`gradle.dart`
`buildGradleAar`, `command.add(aarTask)`), and Gradle runs a task
selector that is only a task name in every project that has the task
([Gradle
docs](https://docs.gradle.org/current/userguide/command_line_interface.html#executing_tasks_in_multi_project_builds):
"You can also run a task for all subprojects using a task selector that
consists of only the task name."). Each plugin publishes from its own
task. The comment at `:247-248` says this.
4. **Tasks, publications and task name** (`configureProject` `:27-37`;
`aarTaskName` `:93-97`; `requestedAarComponents` `:99-103`): AGP adds
one software component for each `singleVariant` declaration, with the
variant's name. `registerAarTask` (`:105-110`) registers
`assembleAar<Variant>` for every component, so a misspelled `--flavor`
gets Gradle's candidate list (`Some candidates are: 'assembleAarDebug',
...`). `requestedAarComponents` returns the components whose
`assembleAar<Variant>` name is in `startParameter.taskNames`; only those
get a publication (`addAarPublication`, `:112-123`), because `publish`
publishes every publication in the project. The check uses the same
function. `aarTaskName` is the only place that builds
`assembleAar<Variant>`; its comment points to the tool's `getAarTaskFor`
(`gradle.dart`). Nothing parses a task name back into a variant. The
`component.name != "all"` filter from `5ba8961b517` is deleted (see "Why
removing the deleted code is safe").
5. **Lazy task registration** (`registerAarTask`):
`project.tasks.register` replaces `project.tasks.create(name:)`. The
configuration action only adds `finalizedBy "publish"`.
`publications.create` runs in `addAarPublication`, not in the action,
because maven-publish registers tasks for each publication and Gradle
rejects that while it realizes a task
(`DefaultTaskContainer#register(String, Class, Action) on task set
cannot be executed in the current context.`, seen when
`publications.create` was inside the action).
6. **Requested-variant check** (`checkProjectsHaveRequestedVariants`,
`:136-175`, called at `:252`): Gradle skips projects that lack the
requested task, so the script checks the module and each Android plugin.
The requested variants are the names of the components that
`requestedAarComponents` returns for those projects. A project without a
requested variant fails the build with a `GradleException` that names
the variant and the project:
- module: `Flutter can't build the AAR for variant 'debug' because
module project ':flutter' has no variant with that name. Its variants
are: demoDebug, demoProfile, demoRelease, fullDebug, fullProfile,
fullRelease. Pass the module's product flavor with --flavor, or omit
--flavor if the module has no product flavors.`
- plugin: `Flutter can't build the AAR for variant 'demoDebug' because
plugin project ':plugin_a' has no variant with that name, so the plugin
AAR that the module depends on can't be published. `flutter build aar`
requires the module and each of its Android plugins to declare the same
product flavors.`
- A requested task that matches no project's variant (for example
`--flavor foo`) is left to Gradle: `Task 'assembleAarFooDebug' not found
in root project 'android_generated' and its subprojects. Some candidates
are: 'assembleAarDebug', 'assembleAarDemoDebug',
'assembleAarFullDebug'.` That is the same error as on `5ba8961b517`. The
check's doc says so. (`3edc95d1fa0` parsed the task-name prefix and
reported a Flutter module error in this case; `da9646aad53` drops the
parsing per review.)
7. **Docs.** Both docs describe behavior only.
- `Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md`: the P8 row of
the phase table and "Features that must break" item 8 are rewritten. The
"Why" starting-state list (L38-39) is unchanged, as in other PRs of the
stack.
-
[`website-page-draft.md`](https://github.com/flutter/flutter/blob/master/docs/platforms/android/website-page-draft.md)
(publishing tracked in
[flutter#193713](flutter#193713)): the
`flutter build aar` section is rewritten. It said Flutter "detects the
overlap and reports it with an actionable error". The code keeps the
build file's declaration instead, so the docs were changed to match the
code. It says (L234-237) that when a build file declares `singleVariant`
for a variant, Flutter keeps that declaration and declares the other
variants, and (L238-241) that the module and each Android plugin need a
variant with the built name.
8. **Tests.** devicelab `build_aar_module_test` gets two final sections:
- With `android.newDsl=true` and `android.builtInKotlin=true` in
`.android/gradle.properties`, and
`android.publishing.singleVariant("release") { withSourcesJar() }` in
`.android/Flutter/build.gradle`, it runs `flutter build aar
--no-profile`. It checks that the edits survive, that release has the
AAR and sources jar and no javadoc jar, that debug has the javadoc jar,
and that the plugin AARs exist for release and debug.
- It replaces that declaration with a `demo` product flavor and checks
both errors: `flutter build aar --no-profile --no-release` fails with
the module error, and the same with `--flavor demo` fails with the
plugin error.

`da9646aad53`'s commit message says the check's doc "says when it
returns and when it throws". That doc sentence was added in
`94a29907065`.

### Bug history (moved out of the docs)

The docs describe behavior only, so the history that `3edc95d1fa0` put
in them is here:
- On `5ba8961b517`, the `afterProject` guard skipped all of Flutter's
`singleVariant` declarations for a project whose build file declared any
variant. For a module with at least one Android plugin that declared
only `release`, every `flutter build aar` failed with `Task with name
'assembleAarDebug' not found in project ':flutter'`. With this PR that
build succeeds (table 1 below).
- On `5ba8961b517`, the `dependsOn` loop failed with `Task with name
'assembleAar<Variant>' not found in project ':<plugin>'` whenever a
plugin lacked any module variant, requested or not, and nothing checked
that the module had the requested variant. With this PR only requested
variants are checked, in the module and in each plugin (table 2 below).

### Audit resolution

[PR 9 draft
audit](reidbaker-agent#9 (comment)),
and stack audit items M4 and L8:

| Item | Resolution |
| --- | --- |
| 1. Eager `.get()` and cross-project task lookup by name | The wiring
is deleted, not made lazy: it was redundant (Changes 3). No `.get()` or
`tasks.named` in the script. The check reads software components, and
`registerAarTask` uses `tasks.register`. |
| 2. Broad `catch (Exception)`; overstated `multipleVariants` warning |
Only AGP's exact duplicate message is ignored (`:130-134`); everything
else is rethrown (`:201-203`). No warning is logged, so there is no
`multipleVariants` claim. |
| 3. Ext property without `flutter.internal.` | No ext property.
`afterProject` fires once per project (instrumented: count 1 for `:`,
`:app`, `:flutter`, `:plugin_a` and the included FGP build), and a
second call would only hit ignored duplicates. The "fires more than once
in composite settings" claim is dropped. |

Why the message match and not the type: AGP reports the duplicate from
`AbstractPublishing.checkSingleVariantUniqueness` through
`SyncIssueReporterImpl`, which throws
`com.android.builder.errors.EvalIssueException` in STANDARD mode. That
class is in `com.android.tools.build:builder`, not in `gradle-api`, and
is not on the init script's classpath. `javap -v` on
`AbstractPublishing` shows the same text in AGP 8.11.1, 9.1.0 and 9.3.1.
AGP checks before it adds the component (8.11.1
`AbstractPublishing.kt:27-35`), so the build file's declaration stays.
If AGP changes the text, the exception is rethrown and the build fails
loudly; devicelab `build_aar_module_test` exercises the path on the
template AGP.

Why not declare first or query: the public `Publishing` DSL in
`gradle-api-9.1.0.jar` has only `singleVariant(String)` and
`singleVariant(String, Function1)`, and `LibraryPublishing` adds only
`multipleVariants(...)` (`javap`), so there is no public way to list
declared variants. Declaring Flutter's variants in `beforeProject` +
`pluginManager.withPlugin("com.android.library")` makes the build file
fail at its own `singleVariant("release")` line with the duplicate error
(probe).

Why not AGP types in the init script: `import
com.android.build.api.dsl.LibraryExtension` fails with `unable to
resolve class`. With `gradle-api:9.3.1` on the `initscript` classpath,
the init script gets its own copy of the class from a different
classloader, so `instanceof` is `false` and
`extensions.getByType(LibraryExtension)` throws `Extension of type
'LibraryExtension' does not exist. Currently registered extension types:
[..., LibraryExtension, ...]` (probes).

`multipleVariants`: `PublishingUtil.ensureComponentNameUniqueness`
errors only when a `multipleVariants` component name equals a
`singleVariant` name. Flutter declares the same `singleVariant`s on
`5ba8961b517` when the build file declares none, so that behavior is
unchanged. It is not tested here.

### Why removing the deleted code is safe

- `android.libraryVariants` does not exist with `newDsl=true`
(reproduced above). AGP 9.1.0 marks its class,
`com.android.build.gradle.LibraryExtension`, `@Deprecated` ("This class
is not used for the public extensions in AGP when android.newDsl=true,
which is the default in AGP 9.0, and will be removed in AGP 10.0."), and
the class is not in `gradle-api-9.1.0.jar`. `singleVariants` is an
internal property (`getSingleVariants()` on
`com.android.build.gradle.internal.dsl.AbstractPublishing`, `javap`). It
resolves from Groovy with `newDsl=true`, so it was not a failure, only
internal API.
- The `dependsOn`: with it removed, the task graph for
`assembleAarRelease` on case (a) has `:plugin_a:assembleAarRelease`,
`:plugin_a:bundleReleaseAar`,
`:plugin_a:publishReleasePublicationToMavenRepository` and
`:plugin_a:publish`. `:flutter:generatePomFileForReleasePublication`
depends on no `:plugin_a` task, and
`:flutter:publishReleasePublicationToMavenRepository` depends only on
the plugin's compile-classpath tasks, so the order between the module's
and the plugin's publishing doesn't matter. The module POM refers to the
plugin by coordinates (`plugin_a_release`). devicelab
`build_aar_module_test`, `build_android_host_app_with_module_aar` and
`android_java17_build_android_host_app_with_module_aar` pass (they check
the plugin AAR/POM and that the module POM depends on
`plugin_with_android_release`).
- The `all` filter (`component.name != "all"`, added in flutter#101891
`606e91ad63d`): AGP 7.x created an automatic multi-variant component
named `all` when a project used no publishing DSL ([`PublishingUtil.kt`
at
`studio-2021.3.1`](https://android.googlesource.com/platform/tools/base/+/refs/tags/studio-2021.3.1/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/utils/PublishingUtil.kt),
`createPublishingInfoForLibrary` and `publishingFeatureOptIn`; same in
the AGP 7.3.1 jar). flutter#101891 published it separately with classifiers and
skipped it for the `_<variant>` publications; flutter#112149 (`8e30cc9c56b`)
removed that publication and kept the skip. AGP 8.0 disabled automatic
components by default ([AGP 8.0.0 release
notes](https://developer.android.com/build/releases/past-releases/agp-8-0-0-release-notes)),
and the AGP 8.11.1, 9.1.1 and 9.3.1 jars list
`android.disableAutomaticComponentCreation` as removed, with no `all`
component code. This script declares `singleVariant` for every variant,
which also turns automatic components off on AGP 7.x. A probe that
prints `project.components` after `projectsEvaluated` on all four matrix
configurations shows only per-variant names (`[debug, profile, release]`
for `:flutter`, `:app` and each plugin, `[demoDebug, …, fullRelease]`
for the flavored module, `[freeDebug, freeProfile, freeRelease]` for
`plugin_flavored`, `[debug, release]` for the root), never `all`.
- `tasks.register`: the publication is created at configuration time
only for a requested task name, as with `tasks.create`, whose
configuration closure ran immediately. Only the task object is created
lazily. Table rows below and the devicelab runs show the same artifacts.

### Removed comments, assertions and code

| Removed | Reason |
| --- | --- |
| `afterProject` comment item 2, "The project has already defined which
variants to publish…", and the `singleVariants.size()` guard it
described | The guard read internal API and skipped all of Flutter's
declarations once a build file declared any. Item 1 of that comment
became "Exit early if the project doesn't have the Android Gradle plugin
applied." |
| The `projectsEvaluated` comment "Configure the
`assembleAar<variantName>` task for each plugin's projects and make the
module's equivalent task depend on the plugin's task.", with the
`libraryVariants.all` loop, both `tasks.named(...).get()` calls, both
`assert(... != null)` and the `dependsOn` | The dependency is redundant
(see above). `tasks.named(...).get()` throws before the asserts could
fail, so they never checked anything. |
| `addAarTask` and its `variantName` local | Split into
`registerAarTask` (every component) and `addAarPublication` (requested
components). The task name comes from `aarTaskName`, which the check
also uses. |
| `addAarTask` comment "This check is required to be able to configure
the archives before `publish` runs.", with the check | The check is
`requestedAarComponents`, applied by the caller before
`addAarPublication`. Its reason is the comment on `addAarPublication`:
"`publish` publishes every publication in the project, so only requested
components get one." |
| The `component.name != "all"` filter in `configureProject`, and the
fragment "(except "all" since that is used to publish artifacts in the
new way)" from "Create a default MavenPublication for the variant" |
Nothing creates an `all` component where this script runs (see "Why
removing the deleted code is safe"). |
| From the migration doc item 8 and the website page draft: the
sentences about the deleted guard, the `Task with name … not found`
failures, and what the deleted code did and did not check | The docs
describe behavior only. The history is in "Bug history" above. |

### Behavioral and compatibility notes

Matrix projects (local scratch): (a) module + Android plugin `plugin_a`;
(b) module with flavors `demo`/`full` + `plugin_a`; (c) module +
`plugin_a` + `plugin_flavored` (flavor `free`); (d) module declaring
`singleVariant("release") { withSourcesJar() }` + `plugin_a`.

1. **A build file that declares `singleVariant` for some variants
builds.** Case (d), `flutter build aar`:

   | Script | Result |
   | --- | --- |
| `5ba8961b517` (AGP 9.3.1 `newDsl=false`) | exit 1: `Task with name
'assembleAarDebug' not found in project ':flutter'.` |
| This PR (all four configurations below) | exit 0. Release has the AAR,
POM and sources jar, and no javadoc jar (the build file's declaration).
Debug and profile have sources and javadoc jars. Plugin AAR/POM for all
three. |

2. **Variant mismatches fail with a Flutter error that names the
project.** Only requested variants are checked. The `5ba8961b517` column
is AGP 9.3.1 with `newDsl=false`.

   | Case | `5ba8961b517` | This PR (all four configurations) |
   | --- | --- | --- |
| (b) `--flavor demo` | `Task with name 'assembleAarDemoDebug' not found
in project ':plugin_a'.` | plugin error for `demoDebug` and `:plugin_a`
|
| (c) default | `Task with name 'assembleAarDebug' not found in project
':plugin_flavored'.` | plugin error for `debug` and `:plugin_flavored` |
| (b) no `--flavor`, `--no-profile --no-release` | `Task with name
'assembleAarDemoDebug' not found in project ':plugin_a'.` | module error
for `debug`, listing the module's variants |
| (c) `--flavor free`, `--no-profile --no-release` | `Task with name
'assembleAarDebug' not found in project ':plugin_flavored'.` | module
error for `freeDebug` |
| (b) `--flavor foo` | Gradle `Task 'assembleAarFooDebug' not found in
root project …` | same Gradle error |

Without the module check (`86523c549fc`), the module-error cases exit 0
with "✓ Built" and a repo that has only the plugin AAR.
3. **Path-qualified requests.** `./gradlew -I aar_init_script.gradle …
:flutter:assembleAarRelease` exits 0 and publishes nothing, on
`5ba8961b517` and on this PR: a publication is created only when
`startParameter.taskNames` contains the bare name. The tool sends only
the bare name (`gradle.dart` `command.add(aarTask)`). Not changed here.
4. **No warnings.** `flutter build aar` hides Gradle output on success
unless `-v` (`ProcessUtils.run` buffers it; `gradle.dart` prints it only
on failure, and verified with a `logger.error` probe). Keeping a build
file's declaration needs no action, so nothing is logged. Errors are
shown.
5. **Configuration cache:** with `org.gradle.configuration-cache=true`
in case (a)'s `.android/gradle.properties` (AGP 9.3.1 / Gradle 9.5.0),
`flutter build aar` reports the same single problem with the
`5ba8961b517` script and with this PR: ``Task
`:flutter:compileFlutterBuildDebug` of type
`com.flutter.gradle.tasks.FlutterTask`: invocation of 'Task.project' at
execution time is unsupported with the configuration cache.`` None comes
from the init script.
6. `newDsl=true` with `builtInKotlin=false` fails before the script with
any Kotlin plugin (`The 'org.jetbrains.kotlin.android' plugin is not
compatible with AGP's 9.0 new DSL`). The devicelab section and matrix
set `builtInKotlin=true`.
7. Related open work: no overlap with
[flutter#191681](flutter#191681)
(`gradle.dart`, `gradle_errors.dart` and tests). No open PR touches
`aar_init_script.gradle`.

### Follow-up candidates (pre-existing, not changed here)

- `allprojects { apply plugin: "maven-publish" }` applies maven-publish
to every project, including ones that never publish.
- The `afterProject` and `projectsEvaluated` hooks reach across
projects, which Gradle Isolated Projects does not allow.
- The `-SNAPSHOT` rewrite of `project.version` in `configureProject`.
- The `engine.realm` and `engine.stamp` file reads at configuration time
in `configureProject`.
- The `is-plugin` branch of `projectsEvaluated` looks unreachable from
the tool.
- The configuration cache problem in FGP `FlutterTask` (note 5).
- `newDsl=true` with KGP (`builtInKotlin=false`), for PR 10.
- Mapping between flavored module variants and plugin variants (for
example, plugin `release` for module `demoRelease`).
- `singleVariant` is declared on the root project and `:app`, which
never publish.
- Gradle output is hidden on success, so nothing the script logs at info
or warn level reaches the user without `-v`.

### Integration test coverage of the changed paths

Searched `dev/devicelab/bin/tasks` and
`packages/flutter_tools/test/integration.shard` for `build aar` and
`assembleAar`.

| Path | Tests |
| --- | --- |
| Library check, publishing declarations, lazy task registration, plugin
publishing without `dependsOn` (`newDsl=false`) | devicelab
`build_aar_module_test`, `build_android_host_app_with_module_aar`,
`android_java17_build_android_host_app_with_module_aar`;
`android_obfuscate_test` |
| `newDsl=true`, a build file's `singleVariant` declaration kept |
devicelab `build_aar_module_test`, `newDsl=true` section |
| Module and plugin variant mismatch errors | devicelab
`build_aar_module_test`, product flavor section |
| AGP 9.1.1 and 8.11.1; a variant that no project has | Manual matrix
only |

`Linux build_aar_module_test` runs in presubmit for this PR: in
`.ci.yaml` its `runIf` includes `dev/**` and
`packages/flutter_tools/**`, and it sets neither `presubmit: false` nor
`bringup`. It also ran locally on the head (below) with `cd
dev/devicelab && ../../bin/dart bin/test_runner.dart test -t
build_aar_module_test --exit`.

### Tests run locally (macOS, JDK 17)

On the head (`b9493299916`; master merged at `4521d8d6634`):

- `gradle test --rerun-tasks` in `packages/flutter_tools/gradle`, with
the engine's `third_party/gradle` binary that the README names: 33
suites, 277 tests, 0 failures.
- `dart format` and `dart analyze --fatal-infos` on
`build_aar_module_test.dart`: clean (unchanged since `3edc95d1fa0`).
- Tool tests: `test/commands.shard/hermetic/build_aar_test.dart`,
`test/commands.shard/permeable/build_aar_test.dart` and
`test/general.shard/android/android_gradle_builder_test.dart`: 56 tests,
all pass.
- `test/integration.shard/android_obfuscate_test.dart`: 2 tests pass.
- devicelab via `bin/test_runner.dart test --exit`, one at a time:
`build_aar_module_test` (including both added sections),
`build_android_host_app_with_module_aar`,
`android_java17_build_android_host_app_with_module_aar`,
`build_android_host_app_with_module_source`,
`module_host_with_custom_build_test`, all `success`.
- The configuration cache comparison (note 5).

Manual matrix on `b9493299916`, four configurations:
- **9.3.1 off:** AGP 9.3.1 / Gradle 9.5.0, `newDsl=false`,
`builtInKotlin=false`.
- **9.3.1 on:** AGP 9.3.1 / Gradle 9.5.0, `newDsl=true`,
`builtInKotlin=true`.
- **9.1.1 on:** AGP 9.1.1 / Gradle 9.3.1 (FGP's minimums on master since
flutter#193813), `newDsl=true`, `builtInKotlin=true`.
- **8.11.1:** AGP 8.11.1 / Gradle 8.14.3, `newDsl=false`, with
`skipDependencyChecks=true` in `.android/gradle.properties`. Without it,
FGP stops before the init script runs: `Your project's Gradle version
(8.14.3) is lower than Flutter's minimum supported version of 9.3.1.`

| Case | 9.3.1 off | 9.3.1 on | 9.1.1 on | 8.11.1 |
| --- | --- | --- | --- | --- |
| (a) | exit 0, AAR, POM, sources and javadoc jars for
`flutter_{debug,profile,release}` and `plugin_a_{debug,profile,release}`
| same | same | same |
| (b) `--flavor demo` | exit 1, plugin error | same | same | same |
| (b) no `--flavor` | exit 1, module error | same | same | same |
| (b) `--flavor foo` | exit 1, Gradle `Task 'assembleAarFooDebug' not
found in root project` | same | same | same |
| (c) | exit 1, plugin error | same | same | same |
| (c) `--flavor free` | exit 1, module error | same | same | same |
| (d) | exit 0, as (a), except module release has the sources jar and no
javadoc jar | same | same | same |

Case (a)'s `flutter_release-1.0.pom` depends on
`dev.pr9.plugin_a:plugin_a_release:1.0` (checked on `b9493299916`, AGP
9.1.1).

Negative control, the master script (same at `5ba8961b517` and
`4521d8d6634`), on `b9493299916`: with `newDsl=true` (AGP 9.3.1 and AGP
9.1.1), (a)-(d) all fail with `Can't generate AAR on a non Android
library project.`; with `newDsl=false` on AGP 9.3.1, (a) passes and
(b)-(d) fail with Gradle's `Task with name 'assembleAar<V>' not found in
project ':<p>'.` On `a17fafcc35e`, with only line 13 changed to
`hasPlugin`, `newDsl=true` fails at line 178 with `Could not get unknown
property 'libraryVariants' for object of type
com.android.build.gradle.internal.dsl.LibraryExtensionImpl$AgpDecorated.`
devicelab `build_aar_module_test` with that script fails at the
`newDsl=true` section with the same error.

</details>

---

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
reidbaker-agent added a commit to reidbaker-agent/flutter that referenced this pull request Oct 22, 2026
…tensions in FlutterPlugin.apply (flutter#194918)

## Description

Part of [flutter#180137](flutter#180137),
[flutter#166550](flutter#166550).

I broke the check in PR 7/11
([flutter#193693](flutter#193693)) in the new
DSL migration for users that were on AGP 8.11. This fixes the mistake
and adds a test. This will likely need to be cherry picked.

<details>
<summary>Agent authored details</summary>

### Related PR

- Follow-up PR:
[flutter#195006](flutter#195006). Because
`flutter#194918` is a cherry-pick candidate, it only replaces the
`OUT_OF_SUPPORT_RANGE_PROPERTY` check in `FlutterPlugin.apply` with a
typed `catch (e: DependencyValidationException)`. Removing
`OUT_OF_SUPPORT_RANGE_PROPERTY` itself—which touches 7 property setters,
a task action, and 14 test assertions across
`DependencyVersionChecker.kt` and `DependencyVersionCheckerTest.kt`—is
split into [flutter#195006](flutter#195006) so
this cherry-pick stays minimal.

### Background

When [flutter#193813](flutter#193813)
(`ce6e766ad1c`) raised the minimum supported AGP version in
`DependencyVersionChecker` to `9.1.1` and bumped the compile-time
`agpVersion` in `packages/flutter_tools/gradle/build.gradle.kts` from
`8.11.1` to `9.1.1`, the Kotlin compiler began emitting AGP 9 binary
method signatures for calls against `ApplicationExtension` (such as
`ApplicationExtension.getDefaultConfig()Lcom/android/build/api/dsl/ApplicationDefaultConfig;`,
whereas in AGP 8.x `CommonExtension.getDefaultConfig()` returned
`DefaultConfig` without a covariant bridge on `ApplicationExtension`).

Because `FlutterPlugin.apply` invoked `addFlutterTasks(project)` (which
calls `configureAbis` -> `configureAbiWithoutSplits` ->
`extension.defaultConfig.ndk` and `DslVersionCodes.from(appExtension)`
introduced in [flutter#193693](flutter#193693)
/ [flutter#192488](flutter#192488)) before
running `DependencyVersionChecker.checkDependencyVersions(project)`,
building an AGP 8.x project crashed during `addFlutterTasks` with:

```text
java.lang.NoSuchMethodError: 'com.android.build.api.dsl.ApplicationDefaultConfig com.android.build.api.dsl.ApplicationExtension.getDefaultConfig()'
```

before reaching
`DependencyVersionChecker.checkDependencyVersions(project)`, preventing
the user from seeing the actionable minimum AGP version error message
and upgrade guidance.

### Changes

1. **`FlutterPlugin.kt`**:
- Moves `FlutterPluginUtils.detectApplyingKotlinGradlePlugin(project)`
and the `shouldSkipDependencyChecks` /
`DependencyVersionChecker.checkDependencyVersions(project)` block to the
beginning of `FlutterPlugin.apply(project)` (immediately after
`this.project = project`), before `addFlutterTasks(project)` and all
`ApplicationExtension` / Android DSL accesses.
- Replaces the `OUT_OF_SUPPORT_RANGE_PROPERTY` extra-property check in
`FlutterPlugin.apply` with a typed `catch (e:
DependencyValidationException)` that rethrows
`DependencyValidationException` directly and logs any other `Exception`,
removing the comment that described `OUT_OF_SUPPORT_RANGE_PROPERTY`.
2. **`FlutterPluginTest.kt`**:
- Adds `FlutterPlugin apply checks dependency versions before
configuring project`, verifying via `verifyOrder` that
`FlutterPluginUtils.detectApplyingKotlinGradlePlugin(project)` and
`DependencyVersionChecker.checkDependencyVersions(project)` run before
task registration, `AndroidComponentsExtension` callbacks
(`finalizeDsl`, `onVariants`), and `ApplicationExtension` DSL
configuration (`defaultConfig`, `buildTypes`).
- Adds `FlutterPlugin apply rethrows DependencyValidationException and
logs other exceptions`, verifying both exception branches without
relying on the extra property, and stubs supported Gradle and AGP
versions in `setupTestProjectEnvironment` /
`setupMockComponentsExtension`.
3. **`packages/flutter_tools/gradle/AGENTS.md`**:
- Updates the `Matrix Verification` bullet on line 40 to reference the
supported AGP and Gradle versions defined in
`DependencyVersionChecker.kt`.

### Verification

- **Unit test negative/positive verification**:
- Moving `DependencyVersionChecker` after `addFlutterTasks` or moving
`detectApplyingKotlinGradlePlugin` after `DependencyVersionChecker`
fails `FlutterPlugin apply checks dependency versions before configuring
project`.
- Reverting the typed `catch (e: DependencyValidationException)` to the
property check fails `FlutterPlugin apply rethrows
DependencyValidationException and logs other exceptions`.
  - `./gradlew test` with the fix applied: all tests pass.
- **End-to-end reproduction with AGP 8.11.1 and Gradle 9.3.1**:
- Before fix: failed at `FlutterPlugin.configureAbiWithoutSplits` with
`java.lang.NoSuchMethodError:
'com.android.build.api.dsl.ApplicationDefaultConfig
com.android.build.api.dsl.ApplicationExtension.getDefaultConfig()'`.
- After fix: fails cleanly with `Error: Your project's Android Gradle
Plugin version (Android Gradle Plugin version 8.11.1) is lower than
Flutter's minimum supported version of Android Gradle Plugin version
9.1.1. Please upgrade your Android Gradle Plugin version.`
- **Formatting**:
- `ktlint 1.5.0` with
`--editorconfig=dev/bots/test/analyze-test-input/.editorconfig
--baseline=dev/bots/test/analyze-test-input/ktlint-baseline.xml` passed
with zero findings.

</details>

---

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/CONTRIBUTING.ai.md
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
…ABI versionCode to the variant API (flutter#193693)

## Description

This is PR 7 of 11 in the AGP 9.1.0 / public `gradle-api` migration
stack ([flutter#180137](flutter#180137),
[flutter#166550](flutter#166550)).

This pr changes the way splitting by api version codes work. This can be
a surprising change but I have not found a work around.

PR 8 adds add-to-app support and as a result deletes a bunch of code
that exists to bridge the partial migration. See
flutter#19371 specifically
https://github.com/flutter/flutter/pull/193717/changes/f668780fd47caca13c03828f49d21572074825d6..a6be16034c8a1a4a0456cd35194b5973cfd4dc75#r4185414230
which is a subset of changes.
<details>
<summary>Agent authored details</summary>

### Changes

1. **flutter-apk copy on the variant API.** `CopyFlutterApksTask`
(`copyFlutterApks<Variant>`) copies the variant's `SingleArtifact.APK`
outputs, read through `BuiltArtifactsLoader`, into
`build/app/outputs/flutter-apk/`. The names are
`app[-abi][-flavor]-<mode>.apk`, the format `listApkPaths` in
`lib/src/android/gradle.dart` expects. `assemble<Variant>` depends on
the task.
- The task has no `Project` or variant fields. It uses injected
`FileSystemOperations` and `@InputDirectory @PathSensitive(RELATIVE)`.
- It is annotated `@DisableCachingByDefault(because = "Not worth
caching")`, the annotation and reason Gradle uses on its own `Copy` and
`Sync` tasks.
- Its declared outputs are the individual APK files (`@OutputFiles`).
The shared `flutter-apk` directory is not declared, because it would
overlap between variants.
- One naming function, `CopyFlutterApksTask.apkFileName`, produces both
the declared outputs and the copied files. Its inputs are the ABIs of
the variant's outputs, the flavor, and the build mode.
2. **Per-ABI versionCode without reading `output.versionCode`.** For
`--split-per-abi`, the plugin sets `output.versionCode =
ABI_VERSION[abi] * 1000 + base` on each `ApplicationVariant` output with
an ABI filter. It never reads the property, because AGP rejects that
read during configuration when
`android.compatibility.enableLegacyApi=false`.
- `DslVersionCodes` takes a snapshot of `defaultConfig.versionCode` and
each product flavor's `versionCode` in `androidComponents.finalizeDsl`.
- `forVariant(variant.productFlavors)` resolves the base the way AGP
merges it: the first flavor, in dimension order, that sets a
versionCode, otherwise `defaultConfig`.
- If the DSL declares no versionCode, Flutter logs a warning and leaves
the variant's versionCodes unchanged.
3. **APK transform errors point at the transform.** The copy fails in
two cases. Both can happen only if another plugin or build script
transforms `SingleArtifact.APK`:
   - the APK directory has no `output-metadata.json`;
- an APK has an ABI filter that none of the variant's outputs declares.

Both error messages name `SingleArtifact.APK` and tell the user to
contact the maintainer of the plugin that does the transform. They do
not ask for a Flutter issue. The task KDoc and `website-page-draft.md`
say what a transform must keep. Plugins Flutter developers commonly use
(Walle, VasDolly, packer-ng, AndResGuard, Sentry, Firebase App
Distribution, Gradle Play Publisher) do not trigger either error; the
evidence is in [this review
reply](flutter#193693 (comment)).
4. **Deleted** the app-path `applicationVariants.configureEach` block
and `configureAbiVersionCodeOverride`, together with their two
`@Suppress("DEPRECATION")` markers and the unsafe `as ApkVariantOutput`
cast. The add-to-app module path (`addFlutterDepsForModule`,
`libraryVariants`) is left for PR 8.
5. **Structure follows the PR 6 review** (@mboetger's point that
`registerFlutterAssetTasks` should not register the compile task).
`onVariants` calls `configureSplitPerAbiVersionCodes` and
`registerCopyFlutterApksTask` after `registerFlutterAssetTasks`, under
the same `isApplicationProject && shouldCompileFlutterForVariant` gate.
Each helper does one job, and no helper takes a parameter only to pass
it on.
6. **Docs:**
- `Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md`: decision
record 4, the replacement map, and items 2 and 3 of "Features that must
break".
- `website-page-draft.md`: versionCode recipes that do not read
`output.versionCode`, and the rules for APK transforms.
- Links to the draft carry a TODO for
[flutter#193713](flutter#193713), which
tracks publishing it on docs.flutter.dev.
7. **Tests:**
   - `CopyFlutterApksTaskTest` (new, 5 tests).
   - `DslVersionCodesTest` (new, 5 tests).
- 8 new `FlutterPluginTest` cases: defaultConfig and flavor offsets, the
no-versionCode warning, no split, the force flag, copy wiring,
profile/custom build type naming, and assemble wiring. The mocked
`VariantOutput.versionCode` stubs only `set`, so any read fails the
test.
- `flutter_build_apk_split_per_abi_test.dart`: a fourth test builds with
`android.compatibility.enableLegacyApi=false`, and every test asserts
each per-ABI APK is in `flutter-apk`.

### Review response (commit `33f39876b92`)

| Comment | Change |
| --- | --- |
| Caching annotation: "How do I know this is true?" | Uses Gradle's own
`Copy`/`Sync` annotation and reason (checked with `javap`). |
| Why would `load` return null? / How would ABIs mismatch? | Comments
say exactly when each can happen. Errors point at the plugin that
transforms the APK. |
| Other places that build APK names | FGP has one. The tool's three
reading sites are listed in the reply (ratchet: not changed here). |
| Custom variants, and proof the copy works | Build-mode rule matches
the deleted code. Results of a flavor and custom build type scratch app
are in the reply. |
| Temporal wording | Removed from KDoc and docs. |
| Link to the website docs | TODO tied to flutter#193713 until the page is
published. |
| Reading `output.versionCode` depends on compatibility mode | No read.
`finalizeDsl` snapshot instead, with a compatibility-mode-off
integration test. |
| Why ×10000, and can the earlier numbers be kept? | Explained in the
replies. Keeping them requires reading the value the app set, which AGP
rejects with compatibility mode off. |

### Why removing the deleted code is safe

The deleted code used `AbstractAppExtension.applicationVariants`,
`ApkVariantOutput.versionCodeOverride`/`getFilter`, and
`packageApplicationProvider`. AGP deprecated these, and they are not
available with AGP 9's `android.newDsl=true`. The replacements
(`ApplicationVariant.outputs`, `VariantOutput.versionCode`/`filters`,
`SingleArtifact.APK`, `BuiltArtifactsLoader`,
`DslLifecycle.finalizeDsl`) are public `com.android.build.api` APIs
available at the stack's AGP 8.11.1 floor.

### Behavioral and compatibility notes

1. **versionCode for apps that transform it in their own `onVariants`
(breaking).** Flutter registers its `onVariants` callback when the
plugin is applied, so AGP runs it before an app's
`androidComponents.onVariants` block. `versionCodeOverride` was applied
after that block. Apps that set `versionCode` only in the `android {}`
DSL see no change. An app that multiplies by 10000 (compatibility mode
on), built with `--build-number 42`:

   | ABI (offset) | `versionCodeOverride` | This PR |
   | --- | --- | --- |
   | `armeabi-v7a` (1) | 421000 | 10420000 |
   | `arm64-v8a` (2) | 422000 | 20420000 |
   | `x86_64` (4) | 424000 | 40420000 |

Values stay distinct, keep ABI order, and only go up, so Play upgrades
keep working. With compatibility mode off, apps can't read
`output.versionCode` either. For that case the docs give a recipe that
sets an absolute value in the app's `onVariants`, which replaces
Flutter's: `offset * 1000 + flutter.versionCode * 10000`.
2. **A versionCode declared only in the manifest is not offset.** The
DSL snapshot can't see it, so Flutter logs a warning and leaves the
variant's versionCodes unchanged.
3. **A `finalizeDsl` callback registered after Flutter's that changes
versionCode is not seen.** This is documented in the `DslVersionCodes`
KDoc and the migration doc.
4. **`copyFlutterApks<Variant>` tasks appear in `gradlew tasks`** and
can be UP-TO-DATE.
5. **Two build types that map to the same Flutter mode share a file
name.** For example, `debug` and a debuggable `staging` both write
`app-debug.apk`; the deleted code had the same clash. Running both
assembles in one `./gradlew` invocation makes the two copy tasks
alternate. The `flutter` tool builds one variant per invocation, so it
is unaffected.

### Integration test coverage of the changed paths

| Path | Tests |
| --- | --- |
| Per-ABI versionCode: default, build number, app `onVariants`
transform, compatibility mode off |
`flutter_build_apk_split_per_abi_test.dart` (4 tests; checks
versionCodes with `apkanalyzer` and the per-ABI files in `flutter-apk`)
|
| flutter-apk copy, default and flavored |
`android_gradle_asset_merging_test.dart`,
`gradle_libapp_so_packaging_test.dart` (incl. flavors),
`android_plugin_example_app_build_test.dart`,
`isolated/native_assets_flutter_build_test.dart`; devicelab
`flavors_test`, `gradle_desugar_classes_test` |
| Plugin unit tests on AGP 8.11.1 and AGP 9 |
`android_run_flutter_gradle_plugin_tests_test.dart` |

### Tests run locally

- `./gradlew test` in `packages/flutter_tools/gradle` (JDK 17): pass (33
suites, 0 failures).
- ktlint 1.5.0 with the CI `.editorconfig` and baseline: clean.
- `dart format` / `dart analyze --fatal-infos` on the changed Dart test:
clean.
- `flutter_build_apk_split_per_abi_test` on `33f39876b92`: 4/4 pass.
- On the review changes before the final edits to comments and error
messages: `android_gradle_asset_merging_test` (4),
`gradle_libapp_so_packaging_test` (4),
`android_run_flutter_gradle_plugin_tests_test` (2) all pass.
- Manual build with `enableLegacyApi=false` (AGP 9.3.1): versionCodes
1042/2042/4042. A negative control that re-adds the read fails with
"configuration of project ':app' has not completed yet".
- Scratch app with `free`/`paid` flavors and a custom `staging` build
type: APK names and versionCodes match the deleted code.

</details>

---

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
… variant API (flutter#193717)

PR 7 ([flutter#193693](flutter#193693)) merged
to `master` as `ecd1ab06865`, and `23ddd6b74e6` merges `master` into
this branch, so the [Files
changed](https://github.com/flutter/flutter/pull/193717/files) tab shows
only PR 8's changes.

PR 8 commits:
- `8c9a54d8297` Unify add-to-app module wiring on the variant API
- `2c28d8f8245` Merge PR 7 head `f668780fd47` (conflicts resolved to PR
7's wording where PR 8 does not change the code)
- `a6be16034c8` Trim comments added by PR 8
- `b8cd39bcf81` Address review: clarify the variant gate and task
registration comments
- `2efbd345505` Address review: check the add-to-app module in
module_host_with_custom_build_test
- `d8f869ec468` Address review: share task registration stubs in
FlutterPluginTest
- `798ac295725` Address review: take ApplicationVariant in
configureApplicationOutputs
- `1366874099e` Address review: drop
android.compatibility.enableLegacyApi from the qa section
- `eb20bbd43de` Merge PR 7 head `25550b3fc49` (no conflicts)
- `23ddd6b74e6` Merge `master` at `ecd1ab06865` (PR 7 squash-merged).
`FlutterPlugin.kt`, `FlutterPluginUtils.kt` and `FlutterPluginTest.kt`
conflicted only because the squash commit is not an ancestor; `master`'s
copies are identical to `25550b3fc49`, already merged by `eb20bbd43de`,
so this branch's side was kept.

## Description

This is PR 8 of 11 in the AGP 9.1.0 / public `gradle-api` migration
stack ([flutter#180137](flutter#180137),
[flutter#166550](flutter#166550)).

An add-to-app module (an Android library) is wired through the same
`androidComponents.onVariants` path as an app. The module adds Flutter's
assets and native libraries to its own variants, and the host app
consumes them like any Android library. Flutter does not look up or
configure the host project. With this PR, a host app with AGP 9's
`android.newDsl=true` builds a Flutter module from source; on
`33f39876b92` it fails with `Check failed.` while configuring `:app`.

**Breaking:** `flutter.hostAppProjectName` has no effect. Setting it
logs a warning.

**Base:** `master`. PR 7 (flutter#193693) merged as `ecd1ab06865`.

<details>
<summary>Agent authored details</summary>

### Changes

1. **Module variants on `onVariants`** (`FlutterPlugin.kt`
`addFlutterTasks`). For every variant, app or library, `onVariants`
calls `registerFlutterCompileTask`, `registerFlutterAssetTasks` and
`registerFlutterJniLibsTask`. Only application variants then call the
new `configureApplicationOutputs` (per-ABI versionCodes and the
flutter-apk copy), which takes an `ApplicationVariant`. There is no
mid-lambda `return@onVariants`.
2. **Every module variant is configured.**
`shouldCompileFlutterForVariant` returns true for a `LibraryVariant`.
The command-line check (`shouldConfigureFlutterTask`, flutter#109560) compares
the command line with the project's own `assemble<Variant>` names, and a
host task such as `:app:assembleDemoStaging` never matches the module
variant it consumes (`debug`, via `matchingFallbacks`). A module has
three variants: the template's `debug` and `release`, plus the `profile`
build type the plugin creates. It declares no flavors
(`templates/module/android/library_new_embedding/Flutter.tmpl/build.gradle.tmpl`).
The tasks are registered lazily, so a build runs only the module variant
the host consumes. `--dry-run` on a host with flavor `demo` and build
types `staging` (falls back to `debug`) and `prod` (falls back to
`release`):

   | Host task | Module Flutter tasks in the task graph |
   | --- | --- |
| `app:assembleDemoDebug`, `app:assembleDemoStaging` |
`compileFlutterBuildDebug`, `copyFlutterAssetsDebug`,
`copyJniLibsflutterBuildDebug` |
| `app:assembleDemoRelease`, `app:assembleDemoProd` |
`compileFlutterBuildRelease`, `copyFlutterAssetsRelease`,
`copyJniLibsflutterBuildRelease` |

3. **Build mode from the module variant.** `registerFlutterCompileTask`
reads the library variant's `buildType` and public
`Component.debuggable`, so the mode follows the module variant the host
consumes.
4. **jniLibs wired from the compile task provider.**
`registerFlutterJniLibsTask` takes `compileTaskProvider` and sets
`intermediateDir` from `compileTaskProvider.map { it.outputDirectory }`.
The `tasks.matching { it.name == … }` dependency and the `findByName`
provider are gone. Both copy tasks are registered only together with
their compile task, so `intermediateDir` is required (not `@Optional`)
in `CopyFlutterJniLibsTask` and `CopyFlutterAssetsTask`, and their
`isPresent` branches are removed.
5. **`flutter.hostAppProjectName`.** `warnIfHostAppProjectNameIsSet`
reads `project.providers.gradleProperty(…)` (not
`rootProject.hasProperty`) and logs `Warning: The Gradle property
'flutter.hostAppProjectName' has no effect. Remove it from
gradle.properties.` The `flutter.hostAppProjectName=SampleApp` line is
removed from
`dev/integration_tests/pure_android_host_apps/android_custom_host_app/gradle.properties`,
so devicelab `module_custom_host_app_name_test` (host project
`:SampleApp`) checks that a host not named `app` builds without the
property.
6. **Deleted:**
- From `FlutterPlugin.kt`: the host-project lookup, the
`appProject.afterEvaluate` × `libraryVariants.all` ×
`applicationVariants.all` loop, the `merge<HostVariant>Assets.dependsOn`
edge, `addFlutterDepsForModule` and `addCopyFlutterAssetsDependency`.
- From `FlutterPluginUtils.kt`: `shouldConfigureFlutterTask(Project,
Task)` and `buildModeFor(com.android.builder.model.BuildType)`. Both
lost their last caller.
7. **Docs.**
- `Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md`: decision
record 5 and "Features that must break" item 6 are rewritten.
-
[`website-page-draft.md`](https://github.com/flutter/flutter/blob/master/docs/platforms/android/website-page-draft.md)
(publishing tracked in
[flutter#193713](flutter#193713)): the
add-to-app section is rewritten.
- The text these replace said that the warning names a removal milestone
(it does not), and that a host `staging` debuggable build type gets
debug artifacts (the module variant decides). It also said that
`tasks.getByPath(":flutter:copyFlutterAssetsDebug")` fails with "Task
with path … not found". That is false: in the scratch host below it
returns a `CopyFlutterAssetsTask`.
8. **Tests.**
   - New `FlutterPluginTest` cases:
- jniLibs wiring from the compile task provider, with no name lookups;
- a module variant configured for a single unrelated command-line task,
with no APK wiring;
     - the module build mode for `debug`, `profile` and `release`;
- the `hostAppProjectName` warning, with no host lookup, and no warning
when it is unset.
   - The skip test also checks that no jniLibs task is registered.
- The new tests share stub helpers (`stubTaskRegistration`,
`captureTaskConfiguration`, `setCommandLineTasks`).
- devicelab `module_host_with_custom_build_test` gets a final section.
The host fixture adds a build type `qa` (debuggable, falls back to
`release`). The section runs `app:assembleDemoQa` with
`android.newDsl=true` and `flutter.hostAppProjectName=app`. It checks
that the output has the warning, and that `app-demo-qa.apk` has the
Flutter assets and `libapp.so` for arm64-v8a and armeabi-v7a, and no
debug assets. The existing sections already check
`app:assembleDemoStaging` (debug artifacts).

### Audit resolution

[PR 8 draft
audit](reidbaker-agent#8 (comment)):

| Item | Resolution |
| --- | --- |
| 1. String-based compile task lookup for jniLibs | Provider wiring,
`FlutterPlugin.kt:549-553`. Unit test `onVariants wires the jniLibs copy
to the output of the variant's compile task`. |
| 2. Mid-block `return@onVariants` | None. `onVariants` (`:329-350`)
calls helpers; app-only wiring is in `configureApplicationOutputs`
(`:647`). |
| 3. Redundant `dependsOn` | None on the assets copy (`:497-530`) or the
jniLibs copy (`:536-560`). |
| 4. Dead overload; `rootProject` read; fixture property |
`shouldConfigureFlutterTask(Project, Task)` deleted.
`providers.gradleProperty` at `:665`. Fixture line removed. |

[PR 7 draft
audit](reidbaker-agent#7 (comment)):
its four items were resolved in flutter#193693. The stale "registered in
`applicationVariants`" KDoc it named has no counterpart here; the
`registerFlutterCompileTask` KDoc names no project type.

### Why removing the deleted code is safe

- The deleted module path used
`com.android.build.gradle.LibraryExtension.libraryVariants`,
`AbstractAppExtension.applicationVariants`,
`BaseVariant.mergeAssetsProvider` and
`BaseVariantOutput.processResourcesProvider`. AGP does not register
those extensions with `android.newDsl=true`, which is why `33f39876b92`
fails with `Check failed.` in the host `afterEvaluate` block that looks
up `LibraryExtension`. Its replacements (`LibraryVariant`,
`Component.debuggable`,
`Sources.assets`/`jniLibs.addGeneratedSourceDirectory`) are public
`com.android.build.api` APIs. FGP compiles against AGP 9.1.1, the
minimum on `master` (flutter#193813), and `Component.getDebuggable` also exists
in gradle-api 8.11.1 (checked with `javap`).
- Remaining `hostAppProjectName` users: none. `git grep
hostAppProjectName` hits only this PR's code, tests and docs, plus an
unrelated iOS `XcodeProject.hostAppProjectName` in the tool. The
templates, `include_flutter.groovy`, `module_plugin_loader.gradle` and
devicelab do not read it. devicelab `module_custom_host_app_name_test`
(host project `:SampleApp`) passes without the fixture property.
- `com.android.builder.model.BuildType` and
`shouldConfigureFlutterTask(Project, Task)` have no remaining
references.

### Removed comments, docstrings and tests

| Removed | Reason |
| --- | --- |
| KDoc and two `TODO(gmackall)` comments (flutter#166550) on
`addFlutterDepsForModule`; KDoc on `addCopyFlutterAssetsDependency` |
Functions deleted. flutter#166550 stays open for the rest of the stack. |
| The "host app build variant → Flutter variant" table comment in the
module block | The mapping is gone; the module variant decides the mode.
|
| KDoc of `shouldConfigureFlutterTask(Project, Task)` and
`buildModeFor(ModelBuildType)` | Functions deleted. |
| `flutterCompileTaskName` KDoc | Nothing references the task by name,
and the one-line name builder needs no doc. |
| flutter#188785 comment in `registerFlutterJniLibsTask`, and the `@Optional`
KDoc and "no Flutter build" comment in `CopyFlutterJniLibsTask` | The
jniLibs task is registered only with its compile task.
`gradle_libapp_so_packaging_test` "app:assembleAndroidTest builds when
no Flutter compile task is configured" still passes. |
| `@Optional` KDoc on `CopyFlutterAssetsTask.intermediateDir` (it kept
`@Optional` for the module migration) | The migration is this PR. |
| `FlutterPluginUtilsTest`: `buildModeFor returns profile if the
BuildType has name profile`, `buildModeFor returns debug if the
BuildType is debuggable`, `buildModeFor returns release if the BuildType
is not debuggable and not named profile` | Overload deleted.
`buildModeFor with a name and debuggable flag prefers the profile name
over debuggability` covers the same cases. |
| `FlutterPluginUtilsTest`: four `shouldConfigureFlutterTask` tests used
a mocked `Task` | Kept with the same names and inputs, calling the
`String` overload. |
| `CopyFlutterAssetsTaskTest`: `clears the destination directory when
there is no flutter build for the variant` | The input is required, so
that case cannot happen. Replaced by `removes staged assets that the
flutter build does not produce`, which keeps the stale-file check. |
| Comments trimmed per review (`a6be16034c8`): the
`PROP_HOST_APP_PROJECT_NAME` KDoc; the `registerFlutterJniLibsTask` KDoc
sentence naming its compile task; the "An application project only has
application variants." comment above the `check`; restatements in the
KDoc of `shouldCompileFlutterForVariant` (PR 7's paragraph restored
verbatim), `registerFlutterCompileTask`, `configureApplicationOutputs`,
`warnIfHostAppProjectNameIsSet` and both `intermediateDir`s; test helper
KDocs and integration test comments; migration doc item 6 | Each
restated the code. What remains is why library variants are ungated, why
the inputs are required, and the test intent. Added code-comment lines
vs `f668780fd47`: 85 → 39. |
|
`packages/flutter_tools/test/integration.shard/android_add_to_app_module_test.dart`
(added by this PR, then deleted per review, `2efbd345505`) | Its `qa`
check moved into devicelab `module_host_with_custom_build_test`, which
builds the same fixture. Its `staging` check duplicated the devicelab
staging section, except for `android.newDsl=true`. The devicelab task
runs presubmit on Linux, Mac and Windows (`.ci.yaml`), like the deleted
test. |
| Rewritten per review (`b8cd39bcf81`): the library paragraph of the
`shouldCompileFlutterForVariant` KDoc, the assets `GradleException`
comment, the `registerFlutterCompileTask` and
`configureApplicationOutputs` KDocs | They explained too little ("What
does this mean?") or restated the code. |
| The `check(variant is ApplicationVariant)` that PR 7 added in the app
`onVariants` path, moved by this PR into `configureApplicationOutputs`
(`798ac295725`) | The call site branches on `variant is
ApplicationVariant`, and the function takes an `ApplicationVariant`, so
the compiler enforces the type. In an application project every variant
is an `ApplicationVariant`, so the APK wiring runs for the same
variants. |
| `-Pandroid.compatibility.enableLegacyApi=false` in the devicelab `qa`
section (`1366874099e`) | AGP 9.1.0, 9.1.1 and 9.3.1 mark the option
`Deprecated(VERSION_10_0)`. Its consumers back the variant API that
`android.newDsl=true` removes. |

### Behavioral and compatibility notes

1. **`flutter.hostAppProjectName` has no effect (breaking).** It logs a
warning and names no removal milestone. Its only use was the host
lookup.
2. **The Flutter build mode follows the module variant the host
consumes.** A host `debug`/`profile`/`release` consumes the module
variant of the same name. A custom host build type gets the module
variant its `matchingFallbacks` select. With a debuggable `qa` that
falls back to `release`, built as the only task, the APKs contain:

   | Build | `flutter_assets` | `libapp.so` | Flutter tasks run |
   | --- | --- | --- | --- |
| `33f39876b92` (`newDsl=false`) | no | no | `compileFlutterBuildDebug`
(not packaged) |
   | This PR | yes | yes | `compileFlutterBuildRelease` |

3. **A host `assemble<Variant>AndroidTest` run on its own runs the
module's `flutter assemble` for that variant.** `--dry-run` of
`:app:assembleDemoDebugAndroidTest` lists
`:flutter:compileFlutterBuildDebug` with this PR, and only
`copyJniLibsflutterBuildDebug` on `33f39876b92`. This is a cost,
accepted because the module cannot map host task names to its own
variants.
4. **Module assets are a generated assets source directory.** The
deleted code copied them into the module's merged-assets output after
`mergeAssets`, and made every build re-run `clean<MergeAssetsTask>`.
Collisions resolve by source-set priority, as for apps since PR 6.
5. **`copyFlutterAssets<V>` in a module is a `CopyFlutterAssetsTask`,
not a `Copy`.** Lookups by name still work; lookups typed as `Copy`
fail.
6. Related open work:
[flutter#191703](flutter#191703) reports an
NPE in `getLegacyAndroidExtension` with `newDsl=true`. That function was
removed earlier in the stack, and this PR removes the next failure on
that path. A module with plugins on `newDsl=true` is not tested here. No
conflict with
[flutter#193682](flutter#193682) or
[flutter#193610](flutter#193610)
(`gradle_errors.dart`); this PR changes no message that the tool
matches. [flutter#184409](flutter#184409)
asks for separate library and app logic in `addFlutterTasks`; this PR
shares `onVariants` and splits only the app outputs.

### Integration test coverage of the changed paths

| Path | Tests |
| --- | --- |
| Module from source, `newDsl=true`, debuggable custom build type on
`release`, `hostAppProjectName` warning | devicelab
`module_host_with_custom_build_test`, `app:assembleDemoQa` section |
| Module from source, flavor and custom build types, debug and release |
devicelab `module_host_with_custom_build_test` (other sections;
`app:assembleDemoStaging` needs the ungated `LibraryVariant`),
`build_android_host_app_with_module_source` |
| Host project not named `app`, without `hostAppProjectName` | devicelab
`module_custom_host_app_name_test` |
| Module AAR (`flutter build aar`) and AAR host | devicelab
`build_aar_module_test`, `build_android_host_app_with_module_aar`;
`android_obfuscate_test`, `android_plugin_new_output_dir_test` |
| App path | `gradle_libapp_so_packaging_test`,
`flutter_build_apk_split_per_abi_test`,
`android_gradle_asset_merging_test`,
`android_run_flutter_gradle_plugin_tests_test` |

### Tests run locally (macOS, JDK 17)

On the head after merging `master` (`23ddd6b74e6`; FGP compiles and
tests against AGP 9.1.1):

- `./gradlew test --rerun-tasks` in `packages/flutter_tools/gradle`: 33
suites, 277 tests, 0 failures.
- ktlint 1.5.0 with the CI `.editorconfig` and baseline: clean.
- `dart format` and `dart analyze --fatal-infos` on
`module_host_with_custom_build_test.dart` and
`gradle_libapp_so_packaging_test.dart`: clean.
- `gradle_libapp_so_packaging_test`: 4 tests pass.
- devicelab, all `success`: `module_host_with_custom_build_test`,
`module_custom_host_app_name_test`,
`build_android_host_app_with_module_source`.

On `1366874099e`:

- `./gradlew test --rerun-tasks` in `packages/flutter_tools/gradle`: 33
suites, 274 tests, 0 failures.
- ktlint 1.5.0 with the CI `.editorconfig` and baseline: clean.
- `dart format` and `dart analyze --fatal-infos` on
`module_host_with_custom_build_test.dart`: clean.
- devicelab `module_host_with_custom_build_test`: `success`. The
`app:assembleDemoQa` section ran `./gradlew app:assembleDemoQa
-Pandroid.newDsl=true -Pflutter.hostAppProjectName=app` and logged the
`flutter.hostAppProjectName` warning.

On `d8f869ec468`:

- `./gradlew test --rerun-tasks` in `packages/flutter_tools/gradle`: 33
suites, 274 tests, 0 failures.
- ktlint 1.5.0 with the CI `.editorconfig` and baseline: clean.
- `dart format` and `dart analyze --fatal-infos` on
`module_host_with_custom_build_test.dart`: clean.
- devicelab `module_host_with_custom_build_test`: `success`. The
`app:assembleDemoQa` section logged the `flutter.hostAppProjectName`
warning.

On `a6be16034c8` (the merge and the trim):

- `./gradlew test` in `packages/flutter_tools/gradle`: 33 suites, 274
tests, 0 failures.
- ktlint 1.5.0 with the CI `.editorconfig` and baseline: clean.
- `dart format` and `dart analyze --fatal-infos` on the changed Dart
files: clean.
- `android_add_to_app_module_test` (deleted in `2efbd345505`): passes.
- devicelab `module_custom_host_app_name_test` (fixture without
`flutter.hostAppProjectName`): `success`, and the log has no
`hostAppProjectName` warning.

On `8c9a54d8297` (before the merge of PR 7's review changes and the
comment trim):

- Integration tests, one at a time, all pass:
`android_add_to_app_module_test` (1), `android_obfuscate_test` (2),
`android_plugin_new_output_dir_test` (1),
`gradle_libapp_so_packaging_test` (4),
`flutter_build_apk_split_per_abi_test` (4),
`android_gradle_asset_merging_test` (4),
`android_run_flutter_gradle_plugin_tests_test` (2).
- `android_add_to_app_module_test` on `33f39876b92`: fails with `Check
failed.`. With `variant is LibraryVariant ||` removed: fails with "does
not contain 'assets/flutter_assets/AssetManifest.bin'".
- devicelab via `bin/test_runner.dart test --exit`, all `success`:
`module_host_with_custom_build_test`,
`module_custom_host_app_name_test`,
`build_android_host_app_with_module_source`, `build_aar_module_test`,
`build_android_host_app_with_module_aar`.
- Scratch module plus the devicelab host fixture (AGP 9.3.1, Gradle
9.5.0):
- Defaults: `assembleDemoStaging` has `kernel_blob.bin` and no
`libapp.so`; `assembleDemoRelease` has `libapp.so` for arm64-v8a,
armeabi-v7a and x86_64.
- Same with `android.compatibility.enableLegacyApi=false`, and with
`android.newDsl=true` added.

</details>

---

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
…Is (flutter#193962)

PR 7 (flutter#193693) merged to master as `ecd1ab06865` and PR 8 (flutter#193717) as
`4249ccbc18a`. This branch merges master at `4521d8d6634`, so the Files
tab shows only PR 9's changes.

PR 9 commits:
- `86523c549fc` Move aar_init_script to public AGP APIs
- `3edc95d1fa0` Check requested AAR variants in the module and each
plugin
- `da9646aad53` Address review: derive requested variants from
components, register AAR tasks lazily
- `94a29907065` Clarify aar_init_script comments from the adversarial
review
- `ea08280d886` Merge commit '23ddd6b74e6' into agp-aar-init-script (PR
8's head with master at `ecd1ab06865`; no conflicts, no change to PR 9's
files)
- `a17fafcc35e` Merge commit '5ba8961b517' into agp-aar-init-script
(master with PR 8 at `4249ccbc18a`; no conflicts, no change to PR 9's
files)
- `77ba449ea4a` Filter requested AAR components before adding tasks
- `adbfb9cd640` Drop the AGP version list from the duplicate-variant
comment
- `f2b7111c51e` Docs: state the AAR variant requirement in plain words
- `cd10f766b5b` Register an AAR task for every variant, publish only
requested ones
- `b9493299916` Merge commit '4521d8d6634' into agp-aar-init-script
(master, including flutter#193649; no conflicts, no change to PR 9's files)

## Description

This is PR 9 of 11 in the AGP 9.1.0 / public `gradle-api` migration
stack ([flutter#180137](flutter#180137),
[flutter#166550](flutter#166550)).

`flutter build aar` passes
`packages/flutter_tools/gradle/aar_init_script.gradle` to Gradle with
`-I`. The script read `android.libraryVariants` and AGP's internal
`publishing.singleVariants`. With AGP 9's `android.newDsl=true`,
`flutter build aar` on a module fails on `5ba8961b517` with `Can't
generate AAR on a non Android library project.` (script line 13), and
then at line 178 (`Could not get unknown property 'libraryVariants'`).
With this PR the script uses only `pluginManager.hasPlugin`,
`Publishing.singleVariant`, software `components` and `tasks.register`,
and the build succeeds with `newDsl` on and off, on AGP 9.3.1 and on
master's minimum AGP 9.1.1, and on AGP 8.11.1 when FGP's version check
is skipped.

**Breaking:** no. Builds that succeed on `5ba8961b517` succeed with the
same artifacts. Two failing cases change: a module or plugin build file
that declares `singleVariant` for only some variants builds, and a
variant mismatch between the module and a plugin fails with a Flutter
error instead of `Task with name … not found`.

**Base:** `master` at `4521d8d6634`. `aar_init_script.gradle`,
`build_aar_module_test.dart` and both docs are the same at `4521d8d6634`
and `5ba8961b517`, so the comparisons below against `5ba8961b517` hold
for `4521d8d6634`.

<details>
<summary>Agent authored details</summary>

### Changes

All in `aar_init_script.gradle` unless noted. Line numbers are at
`b9493299916`.

1. **Library check** (`:13`):
`project.pluginManager.hasPlugin("com.android.library")` replaces
`android.hasProperty("libraryVariants")`. The error text is unchanged.
2. **Publishing declarations** (`afterProject`, `:185-219`): Flutter
calls `android.publishing.singleVariant(<variant>) { withSourcesJar();
withJavadocJar() }` for each variant, one variant at a time. If the
project's build file already declared that variant, AGP throws, and
`isDuplicateSingleVariantError` (`:125-134`) matches AGP's exact
message, so the build file's declaration is kept. Its comment links the
AGP source of the message ([`AbstractPublishing.kt` at
`studio-2025.3.2`, AGP
9.1.0](https://android.googlesource.com/platform/tools/base/+/refs/tags/studio-2025.3.2/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dsl/AbstractPublishing.kt#38)).
Any other exception is rethrown (`:201-203`). The guard that read
`publishing.singleVariants` and skipped every Flutter declaration once
the build file declared any variant is deleted.
3. **No module-to-plugin `dependsOn`** (`projectsEvaluated`): the loop
over `moduleProject.android.libraryVariants` that made each module
`assembleAar<Variant>` depend on the plugin's task (with eager
`tasks.named(...).get()`) is deleted. The tool requests
`assembleAar<Variant>` without a project path (`gradle.dart`
`buildGradleAar`, `command.add(aarTask)`), and Gradle runs a task
selector that is only a task name in every project that has the task
([Gradle
docs](https://docs.gradle.org/current/userguide/command_line_interface.html#executing_tasks_in_multi_project_builds):
"You can also run a task for all subprojects using a task selector that
consists of only the task name."). Each plugin publishes from its own
task. The comment at `:247-248` says this.
4. **Tasks, publications and task name** (`configureProject` `:27-37`;
`aarTaskName` `:93-97`; `requestedAarComponents` `:99-103`): AGP adds
one software component for each `singleVariant` declaration, with the
variant's name. `registerAarTask` (`:105-110`) registers
`assembleAar<Variant>` for every component, so a misspelled `--flavor`
gets Gradle's candidate list (`Some candidates are: 'assembleAarDebug',
...`). `requestedAarComponents` returns the components whose
`assembleAar<Variant>` name is in `startParameter.taskNames`; only those
get a publication (`addAarPublication`, `:112-123`), because `publish`
publishes every publication in the project. The check uses the same
function. `aarTaskName` is the only place that builds
`assembleAar<Variant>`; its comment points to the tool's `getAarTaskFor`
(`gradle.dart`). Nothing parses a task name back into a variant. The
`component.name != "all"` filter from `5ba8961b517` is deleted (see "Why
removing the deleted code is safe").
5. **Lazy task registration** (`registerAarTask`):
`project.tasks.register` replaces `project.tasks.create(name:)`. The
configuration action only adds `finalizedBy "publish"`.
`publications.create` runs in `addAarPublication`, not in the action,
because maven-publish registers tasks for each publication and Gradle
rejects that while it realizes a task
(`DefaultTaskContainer#register(String, Class, Action) on task set
cannot be executed in the current context.`, seen when
`publications.create` was inside the action).
6. **Requested-variant check** (`checkProjectsHaveRequestedVariants`,
`:136-175`, called at `:252`): Gradle skips projects that lack the
requested task, so the script checks the module and each Android plugin.
The requested variants are the names of the components that
`requestedAarComponents` returns for those projects. A project without a
requested variant fails the build with a `GradleException` that names
the variant and the project:
- module: `Flutter can't build the AAR for variant 'debug' because
module project ':flutter' has no variant with that name. Its variants
are: demoDebug, demoProfile, demoRelease, fullDebug, fullProfile,
fullRelease. Pass the module's product flavor with --flavor, or omit
--flavor if the module has no product flavors.`
- plugin: `Flutter can't build the AAR for variant 'demoDebug' because
plugin project ':plugin_a' has no variant with that name, so the plugin
AAR that the module depends on can't be published. `flutter build aar`
requires the module and each of its Android plugins to declare the same
product flavors.`
- A requested task that matches no project's variant (for example
`--flavor foo`) is left to Gradle: `Task 'assembleAarFooDebug' not found
in root project 'android_generated' and its subprojects. Some candidates
are: 'assembleAarDebug', 'assembleAarDemoDebug',
'assembleAarFullDebug'.` That is the same error as on `5ba8961b517`. The
check's doc says so. (`3edc95d1fa0` parsed the task-name prefix and
reported a Flutter module error in this case; `da9646aad53` drops the
parsing per review.)
7. **Docs.** Both docs describe behavior only.
- `Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md`: the P8 row of
the phase table and "Features that must break" item 8 are rewritten. The
"Why" starting-state list (L38-39) is unchanged, as in other PRs of the
stack.
-
[`website-page-draft.md`](https://github.com/flutter/flutter/blob/master/docs/platforms/android/website-page-draft.md)
(publishing tracked in
[flutter#193713](flutter#193713)): the
`flutter build aar` section is rewritten. It said Flutter "detects the
overlap and reports it with an actionable error". The code keeps the
build file's declaration instead, so the docs were changed to match the
code. It says (L234-237) that when a build file declares `singleVariant`
for a variant, Flutter keeps that declaration and declares the other
variants, and (L238-241) that the module and each Android plugin need a
variant with the built name.
8. **Tests.** devicelab `build_aar_module_test` gets two final sections:
- With `android.newDsl=true` and `android.builtInKotlin=true` in
`.android/gradle.properties`, and
`android.publishing.singleVariant("release") { withSourcesJar() }` in
`.android/Flutter/build.gradle`, it runs `flutter build aar
--no-profile`. It checks that the edits survive, that release has the
AAR and sources jar and no javadoc jar, that debug has the javadoc jar,
and that the plugin AARs exist for release and debug.
- It replaces that declaration with a `demo` product flavor and checks
both errors: `flutter build aar --no-profile --no-release` fails with
the module error, and the same with `--flavor demo` fails with the
plugin error.

`da9646aad53`'s commit message says the check's doc "says when it
returns and when it throws". That doc sentence was added in
`94a29907065`.

### Bug history (moved out of the docs)

The docs describe behavior only, so the history that `3edc95d1fa0` put
in them is here:
- On `5ba8961b517`, the `afterProject` guard skipped all of Flutter's
`singleVariant` declarations for a project whose build file declared any
variant. For a module with at least one Android plugin that declared
only `release`, every `flutter build aar` failed with `Task with name
'assembleAarDebug' not found in project ':flutter'`. With this PR that
build succeeds (table 1 below).
- On `5ba8961b517`, the `dependsOn` loop failed with `Task with name
'assembleAar<Variant>' not found in project ':<plugin>'` whenever a
plugin lacked any module variant, requested or not, and nothing checked
that the module had the requested variant. With this PR only requested
variants are checked, in the module and in each plugin (table 2 below).

### Audit resolution

[PR 9 draft
audit](reidbaker-agent#9 (comment)),
and stack audit items M4 and L8:

| Item | Resolution |
| --- | --- |
| 1. Eager `.get()` and cross-project task lookup by name | The wiring
is deleted, not made lazy: it was redundant (Changes 3). No `.get()` or
`tasks.named` in the script. The check reads software components, and
`registerAarTask` uses `tasks.register`. |
| 2. Broad `catch (Exception)`; overstated `multipleVariants` warning |
Only AGP's exact duplicate message is ignored (`:130-134`); everything
else is rethrown (`:201-203`). No warning is logged, so there is no
`multipleVariants` claim. |
| 3. Ext property without `flutter.internal.` | No ext property.
`afterProject` fires once per project (instrumented: count 1 for `:`,
`:app`, `:flutter`, `:plugin_a` and the included FGP build), and a
second call would only hit ignored duplicates. The "fires more than once
in composite settings" claim is dropped. |

Why the message match and not the type: AGP reports the duplicate from
`AbstractPublishing.checkSingleVariantUniqueness` through
`SyncIssueReporterImpl`, which throws
`com.android.builder.errors.EvalIssueException` in STANDARD mode. That
class is in `com.android.tools.build:builder`, not in `gradle-api`, and
is not on the init script's classpath. `javap -v` on
`AbstractPublishing` shows the same text in AGP 8.11.1, 9.1.0 and 9.3.1.
AGP checks before it adds the component (8.11.1
`AbstractPublishing.kt:27-35`), so the build file's declaration stays.
If AGP changes the text, the exception is rethrown and the build fails
loudly; devicelab `build_aar_module_test` exercises the path on the
template AGP.

Why not declare first or query: the public `Publishing` DSL in
`gradle-api-9.1.0.jar` has only `singleVariant(String)` and
`singleVariant(String, Function1)`, and `LibraryPublishing` adds only
`multipleVariants(...)` (`javap`), so there is no public way to list
declared variants. Declaring Flutter's variants in `beforeProject` +
`pluginManager.withPlugin("com.android.library")` makes the build file
fail at its own `singleVariant("release")` line with the duplicate error
(probe).

Why not AGP types in the init script: `import
com.android.build.api.dsl.LibraryExtension` fails with `unable to
resolve class`. With `gradle-api:9.3.1` on the `initscript` classpath,
the init script gets its own copy of the class from a different
classloader, so `instanceof` is `false` and
`extensions.getByType(LibraryExtension)` throws `Extension of type
'LibraryExtension' does not exist. Currently registered extension types:
[..., LibraryExtension, ...]` (probes).

`multipleVariants`: `PublishingUtil.ensureComponentNameUniqueness`
errors only when a `multipleVariants` component name equals a
`singleVariant` name. Flutter declares the same `singleVariant`s on
`5ba8961b517` when the build file declares none, so that behavior is
unchanged. It is not tested here.

### Why removing the deleted code is safe

- `android.libraryVariants` does not exist with `newDsl=true`
(reproduced above). AGP 9.1.0 marks its class,
`com.android.build.gradle.LibraryExtension`, `@Deprecated` ("This class
is not used for the public extensions in AGP when android.newDsl=true,
which is the default in AGP 9.0, and will be removed in AGP 10.0."), and
the class is not in `gradle-api-9.1.0.jar`. `singleVariants` is an
internal property (`getSingleVariants()` on
`com.android.build.gradle.internal.dsl.AbstractPublishing`, `javap`). It
resolves from Groovy with `newDsl=true`, so it was not a failure, only
internal API.
- The `dependsOn`: with it removed, the task graph for
`assembleAarRelease` on case (a) has `:plugin_a:assembleAarRelease`,
`:plugin_a:bundleReleaseAar`,
`:plugin_a:publishReleasePublicationToMavenRepository` and
`:plugin_a:publish`. `:flutter:generatePomFileForReleasePublication`
depends on no `:plugin_a` task, and
`:flutter:publishReleasePublicationToMavenRepository` depends only on
the plugin's compile-classpath tasks, so the order between the module's
and the plugin's publishing doesn't matter. The module POM refers to the
plugin by coordinates (`plugin_a_release`). devicelab
`build_aar_module_test`, `build_android_host_app_with_module_aar` and
`android_java17_build_android_host_app_with_module_aar` pass (they check
the plugin AAR/POM and that the module POM depends on
`plugin_with_android_release`).
- The `all` filter (`component.name != "all"`, added in flutter#101891
`606e91ad63d`): AGP 7.x created an automatic multi-variant component
named `all` when a project used no publishing DSL ([`PublishingUtil.kt`
at
`studio-2021.3.1`](https://android.googlesource.com/platform/tools/base/+/refs/tags/studio-2021.3.1/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/utils/PublishingUtil.kt),
`createPublishingInfoForLibrary` and `publishingFeatureOptIn`; same in
the AGP 7.3.1 jar). flutter#101891 published it separately with classifiers and
skipped it for the `_<variant>` publications; flutter#112149 (`8e30cc9c56b`)
removed that publication and kept the skip. AGP 8.0 disabled automatic
components by default ([AGP 8.0.0 release
notes](https://developer.android.com/build/releases/past-releases/agp-8-0-0-release-notes)),
and the AGP 8.11.1, 9.1.1 and 9.3.1 jars list
`android.disableAutomaticComponentCreation` as removed, with no `all`
component code. This script declares `singleVariant` for every variant,
which also turns automatic components off on AGP 7.x. A probe that
prints `project.components` after `projectsEvaluated` on all four matrix
configurations shows only per-variant names (`[debug, profile, release]`
for `:flutter`, `:app` and each plugin, `[demoDebug, …, fullRelease]`
for the flavored module, `[freeDebug, freeProfile, freeRelease]` for
`plugin_flavored`, `[debug, release]` for the root), never `all`.
- `tasks.register`: the publication is created at configuration time
only for a requested task name, as with `tasks.create`, whose
configuration closure ran immediately. Only the task object is created
lazily. Table rows below and the devicelab runs show the same artifacts.

### Removed comments, assertions and code

| Removed | Reason |
| --- | --- |
| `afterProject` comment item 2, "The project has already defined which
variants to publish…", and the `singleVariants.size()` guard it
described | The guard read internal API and skipped all of Flutter's
declarations once a build file declared any. Item 1 of that comment
became "Exit early if the project doesn't have the Android Gradle plugin
applied." |
| The `projectsEvaluated` comment "Configure the
`assembleAar<variantName>` task for each plugin's projects and make the
module's equivalent task depend on the plugin's task.", with the
`libraryVariants.all` loop, both `tasks.named(...).get()` calls, both
`assert(... != null)` and the `dependsOn` | The dependency is redundant
(see above). `tasks.named(...).get()` throws before the asserts could
fail, so they never checked anything. |
| `addAarTask` and its `variantName` local | Split into
`registerAarTask` (every component) and `addAarPublication` (requested
components). The task name comes from `aarTaskName`, which the check
also uses. |
| `addAarTask` comment "This check is required to be able to configure
the archives before `publish` runs.", with the check | The check is
`requestedAarComponents`, applied by the caller before
`addAarPublication`. Its reason is the comment on `addAarPublication`:
"`publish` publishes every publication in the project, so only requested
components get one." |
| The `component.name != "all"` filter in `configureProject`, and the
fragment "(except "all" since that is used to publish artifacts in the
new way)" from "Create a default MavenPublication for the variant" |
Nothing creates an `all` component where this script runs (see "Why
removing the deleted code is safe"). |
| From the migration doc item 8 and the website page draft: the
sentences about the deleted guard, the `Task with name … not found`
failures, and what the deleted code did and did not check | The docs
describe behavior only. The history is in "Bug history" above. |

### Behavioral and compatibility notes

Matrix projects (local scratch): (a) module + Android plugin `plugin_a`;
(b) module with flavors `demo`/`full` + `plugin_a`; (c) module +
`plugin_a` + `plugin_flavored` (flavor `free`); (d) module declaring
`singleVariant("release") { withSourcesJar() }` + `plugin_a`.

1. **A build file that declares `singleVariant` for some variants
builds.** Case (d), `flutter build aar`:

   | Script | Result |
   | --- | --- |
| `5ba8961b517` (AGP 9.3.1 `newDsl=false`) | exit 1: `Task with name
'assembleAarDebug' not found in project ':flutter'.` |
| This PR (all four configurations below) | exit 0. Release has the AAR,
POM and sources jar, and no javadoc jar (the build file's declaration).
Debug and profile have sources and javadoc jars. Plugin AAR/POM for all
three. |

2. **Variant mismatches fail with a Flutter error that names the
project.** Only requested variants are checked. The `5ba8961b517` column
is AGP 9.3.1 with `newDsl=false`.

   | Case | `5ba8961b517` | This PR (all four configurations) |
   | --- | --- | --- |
| (b) `--flavor demo` | `Task with name 'assembleAarDemoDebug' not found
in project ':plugin_a'.` | plugin error for `demoDebug` and `:plugin_a`
|
| (c) default | `Task with name 'assembleAarDebug' not found in project
':plugin_flavored'.` | plugin error for `debug` and `:plugin_flavored` |
| (b) no `--flavor`, `--no-profile --no-release` | `Task with name
'assembleAarDemoDebug' not found in project ':plugin_a'.` | module error
for `debug`, listing the module's variants |
| (c) `--flavor free`, `--no-profile --no-release` | `Task with name
'assembleAarDebug' not found in project ':plugin_flavored'.` | module
error for `freeDebug` |
| (b) `--flavor foo` | Gradle `Task 'assembleAarFooDebug' not found in
root project …` | same Gradle error |

Without the module check (`86523c549fc`), the module-error cases exit 0
with "✓ Built" and a repo that has only the plugin AAR.
3. **Path-qualified requests.** `./gradlew -I aar_init_script.gradle …
:flutter:assembleAarRelease` exits 0 and publishes nothing, on
`5ba8961b517` and on this PR: a publication is created only when
`startParameter.taskNames` contains the bare name. The tool sends only
the bare name (`gradle.dart` `command.add(aarTask)`). Not changed here.
4. **No warnings.** `flutter build aar` hides Gradle output on success
unless `-v` (`ProcessUtils.run` buffers it; `gradle.dart` prints it only
on failure, and verified with a `logger.error` probe). Keeping a build
file's declaration needs no action, so nothing is logged. Errors are
shown.
5. **Configuration cache:** with `org.gradle.configuration-cache=true`
in case (a)'s `.android/gradle.properties` (AGP 9.3.1 / Gradle 9.5.0),
`flutter build aar` reports the same single problem with the
`5ba8961b517` script and with this PR: ``Task
`:flutter:compileFlutterBuildDebug` of type
`com.flutter.gradle.tasks.FlutterTask`: invocation of 'Task.project' at
execution time is unsupported with the configuration cache.`` None comes
from the init script.
6. `newDsl=true` with `builtInKotlin=false` fails before the script with
any Kotlin plugin (`The 'org.jetbrains.kotlin.android' plugin is not
compatible with AGP's 9.0 new DSL`). The devicelab section and matrix
set `builtInKotlin=true`.
7. Related open work: no overlap with
[flutter#191681](flutter#191681)
(`gradle.dart`, `gradle_errors.dart` and tests). No open PR touches
`aar_init_script.gradle`.

### Follow-up candidates (pre-existing, not changed here)

- `allprojects { apply plugin: "maven-publish" }` applies maven-publish
to every project, including ones that never publish.
- The `afterProject` and `projectsEvaluated` hooks reach across
projects, which Gradle Isolated Projects does not allow.
- The `-SNAPSHOT` rewrite of `project.version` in `configureProject`.
- The `engine.realm` and `engine.stamp` file reads at configuration time
in `configureProject`.
- The `is-plugin` branch of `projectsEvaluated` looks unreachable from
the tool.
- The configuration cache problem in FGP `FlutterTask` (note 5).
- `newDsl=true` with KGP (`builtInKotlin=false`), for PR 10.
- Mapping between flavored module variants and plugin variants (for
example, plugin `release` for module `demoRelease`).
- `singleVariant` is declared on the root project and `:app`, which
never publish.
- Gradle output is hidden on success, so nothing the script logs at info
or warn level reaches the user without `-v`.

### Integration test coverage of the changed paths

Searched `dev/devicelab/bin/tasks` and
`packages/flutter_tools/test/integration.shard` for `build aar` and
`assembleAar`.

| Path | Tests |
| --- | --- |
| Library check, publishing declarations, lazy task registration, plugin
publishing without `dependsOn` (`newDsl=false`) | devicelab
`build_aar_module_test`, `build_android_host_app_with_module_aar`,
`android_java17_build_android_host_app_with_module_aar`;
`android_obfuscate_test` |
| `newDsl=true`, a build file's `singleVariant` declaration kept |
devicelab `build_aar_module_test`, `newDsl=true` section |
| Module and plugin variant mismatch errors | devicelab
`build_aar_module_test`, product flavor section |
| AGP 9.1.1 and 8.11.1; a variant that no project has | Manual matrix
only |

`Linux build_aar_module_test` runs in presubmit for this PR: in
`.ci.yaml` its `runIf` includes `dev/**` and
`packages/flutter_tools/**`, and it sets neither `presubmit: false` nor
`bringup`. It also ran locally on the head (below) with `cd
dev/devicelab && ../../bin/dart bin/test_runner.dart test -t
build_aar_module_test --exit`.

### Tests run locally (macOS, JDK 17)

On the head (`b9493299916`; master merged at `4521d8d6634`):

- `gradle test --rerun-tasks` in `packages/flutter_tools/gradle`, with
the engine's `third_party/gradle` binary that the README names: 33
suites, 277 tests, 0 failures.
- `dart format` and `dart analyze --fatal-infos` on
`build_aar_module_test.dart`: clean (unchanged since `3edc95d1fa0`).
- Tool tests: `test/commands.shard/hermetic/build_aar_test.dart`,
`test/commands.shard/permeable/build_aar_test.dart` and
`test/general.shard/android/android_gradle_builder_test.dart`: 56 tests,
all pass.
- `test/integration.shard/android_obfuscate_test.dart`: 2 tests pass.
- devicelab via `bin/test_runner.dart test --exit`, one at a time:
`build_aar_module_test` (including both added sections),
`build_android_host_app_with_module_aar`,
`android_java17_build_android_host_app_with_module_aar`,
`build_android_host_app_with_module_source`,
`module_host_with_custom_build_test`, all `success`.
- The configuration cache comparison (note 5).

Manual matrix on `b9493299916`, four configurations:
- **9.3.1 off:** AGP 9.3.1 / Gradle 9.5.0, `newDsl=false`,
`builtInKotlin=false`.
- **9.3.1 on:** AGP 9.3.1 / Gradle 9.5.0, `newDsl=true`,
`builtInKotlin=true`.
- **9.1.1 on:** AGP 9.1.1 / Gradle 9.3.1 (FGP's minimums on master since
flutter#193813), `newDsl=true`, `builtInKotlin=true`.
- **8.11.1:** AGP 8.11.1 / Gradle 8.14.3, `newDsl=false`, with
`skipDependencyChecks=true` in `.android/gradle.properties`. Without it,
FGP stops before the init script runs: `Your project's Gradle version
(8.14.3) is lower than Flutter's minimum supported version of 9.3.1.`

| Case | 9.3.1 off | 9.3.1 on | 9.1.1 on | 8.11.1 |
| --- | --- | --- | --- | --- |
| (a) | exit 0, AAR, POM, sources and javadoc jars for
`flutter_{debug,profile,release}` and `plugin_a_{debug,profile,release}`
| same | same | same |
| (b) `--flavor demo` | exit 1, plugin error | same | same | same |
| (b) no `--flavor` | exit 1, module error | same | same | same |
| (b) `--flavor foo` | exit 1, Gradle `Task 'assembleAarFooDebug' not
found in root project` | same | same | same |
| (c) | exit 1, plugin error | same | same | same |
| (c) `--flavor free` | exit 1, module error | same | same | same |
| (d) | exit 0, as (a), except module release has the sources jar and no
javadoc jar | same | same | same |

Case (a)'s `flutter_release-1.0.pom` depends on
`dev.pr9.plugin_a:plugin_a_release:1.0` (checked on `b9493299916`, AGP
9.1.1).

Negative control, the master script (same at `5ba8961b517` and
`4521d8d6634`), on `b9493299916`: with `newDsl=true` (AGP 9.3.1 and AGP
9.1.1), (a)-(d) all fail with `Can't generate AAR on a non Android
library project.`; with `newDsl=false` on AGP 9.3.1, (a) passes and
(b)-(d) fail with Gradle's `Task with name 'assembleAar<V>' not found in
project ':<p>'.` On `a17fafcc35e`, with only line 13 changed to
`hasPlugin`, `newDsl=true` fails at line 178 with `Could not get unknown
property 'libraryVariants' for object of type
com.android.build.gradle.internal.dsl.LibraryExtensionImpl$AgpDecorated.`
devicelab `build_aar_module_test` with that script fails at the
`newDsl=true` section with the same error.

</details>

---

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
…tensions in FlutterPlugin.apply (flutter#194918)

## Description

Part of [flutter#180137](flutter#180137),
[flutter#166550](flutter#166550).

I broke the check in PR 7/11
([flutter#193693](flutter#193693)) in the new
DSL migration for users that were on AGP 8.11. This fixes the mistake
and adds a test. This will likely need to be cherry picked.

<details>
<summary>Agent authored details</summary>

### Related PR

- Follow-up PR:
[flutter#195006](flutter#195006). Because
`flutter#194918` is a cherry-pick candidate, it only replaces the
`OUT_OF_SUPPORT_RANGE_PROPERTY` check in `FlutterPlugin.apply` with a
typed `catch (e: DependencyValidationException)`. Removing
`OUT_OF_SUPPORT_RANGE_PROPERTY` itself—which touches 7 property setters,
a task action, and 14 test assertions across
`DependencyVersionChecker.kt` and `DependencyVersionCheckerTest.kt`—is
split into [flutter#195006](flutter#195006) so
this cherry-pick stays minimal.

### Background

When [flutter#193813](flutter#193813)
(`ce6e766ad1c`) raised the minimum supported AGP version in
`DependencyVersionChecker` to `9.1.1` and bumped the compile-time
`agpVersion` in `packages/flutter_tools/gradle/build.gradle.kts` from
`8.11.1` to `9.1.1`, the Kotlin compiler began emitting AGP 9 binary
method signatures for calls against `ApplicationExtension` (such as
`ApplicationExtension.getDefaultConfig()Lcom/android/build/api/dsl/ApplicationDefaultConfig;`,
whereas in AGP 8.x `CommonExtension.getDefaultConfig()` returned
`DefaultConfig` without a covariant bridge on `ApplicationExtension`).

Because `FlutterPlugin.apply` invoked `addFlutterTasks(project)` (which
calls `configureAbis` -> `configureAbiWithoutSplits` ->
`extension.defaultConfig.ndk` and `DslVersionCodes.from(appExtension)`
introduced in [flutter#193693](flutter#193693)
/ [flutter#192488](flutter#192488)) before
running `DependencyVersionChecker.checkDependencyVersions(project)`,
building an AGP 8.x project crashed during `addFlutterTasks` with:

```text
java.lang.NoSuchMethodError: 'com.android.build.api.dsl.ApplicationDefaultConfig com.android.build.api.dsl.ApplicationExtension.getDefaultConfig()'
```

before reaching
`DependencyVersionChecker.checkDependencyVersions(project)`, preventing
the user from seeing the actionable minimum AGP version error message
and upgrade guidance.

### Changes

1. **`FlutterPlugin.kt`**:
- Moves `FlutterPluginUtils.detectApplyingKotlinGradlePlugin(project)`
and the `shouldSkipDependencyChecks` /
`DependencyVersionChecker.checkDependencyVersions(project)` block to the
beginning of `FlutterPlugin.apply(project)` (immediately after
`this.project = project`), before `addFlutterTasks(project)` and all
`ApplicationExtension` / Android DSL accesses.
- Replaces the `OUT_OF_SUPPORT_RANGE_PROPERTY` extra-property check in
`FlutterPlugin.apply` with a typed `catch (e:
DependencyValidationException)` that rethrows
`DependencyValidationException` directly and logs any other `Exception`,
removing the comment that described `OUT_OF_SUPPORT_RANGE_PROPERTY`.
2. **`FlutterPluginTest.kt`**:
- Adds `FlutterPlugin apply checks dependency versions before
configuring project`, verifying via `verifyOrder` that
`FlutterPluginUtils.detectApplyingKotlinGradlePlugin(project)` and
`DependencyVersionChecker.checkDependencyVersions(project)` run before
task registration, `AndroidComponentsExtension` callbacks
(`finalizeDsl`, `onVariants`), and `ApplicationExtension` DSL
configuration (`defaultConfig`, `buildTypes`).
- Adds `FlutterPlugin apply rethrows DependencyValidationException and
logs other exceptions`, verifying both exception branches without
relying on the extra property, and stubs supported Gradle and AGP
versions in `setupTestProjectEnvironment` /
`setupMockComponentsExtension`.
3. **`packages/flutter_tools/gradle/AGENTS.md`**:
- Updates the `Matrix Verification` bullet on line 40 to reference the
supported AGP and Gradle versions defined in
`DependencyVersionChecker.kt`.

### Verification

- **Unit test negative/positive verification**:
- Moving `DependencyVersionChecker` after `addFlutterTasks` or moving
`detectApplyingKotlinGradlePlugin` after `DependencyVersionChecker`
fails `FlutterPlugin apply checks dependency versions before configuring
project`.
- Reverting the typed `catch (e: DependencyValidationException)` to the
property check fails `FlutterPlugin apply rethrows
DependencyValidationException and logs other exceptions`.
  - `./gradlew test` with the fix applied: all tests pass.
- **End-to-end reproduction with AGP 8.11.1 and Gradle 9.3.1**:
- Before fix: failed at `FlutterPlugin.configureAbiWithoutSplits` with
`java.lang.NoSuchMethodError:
'com.android.build.api.dsl.ApplicationDefaultConfig
com.android.build.api.dsl.ApplicationExtension.getDefaultConfig()'`.
- After fix: fails cleanly with `Error: Your project's Android Gradle
Plugin version (Android Gradle Plugin version 8.11.1) is lower than
Flutter's minimum supported version of Android Gradle Plugin version
9.1.1. Please upgrade your Android Gradle Plugin version.`
- **Formatting**:
- `ktlint 1.5.0` with
`--editorconfig=dev/bots/test/analyze-test-input/.editorconfig
--baseline=dev/bots/test/analyze-test-input/ktlint-baseline.xml` passed
with zero findings.

</details>

---

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/CONTRIBUTING.ai.md
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c: API break Backwards-incompatible API changes CICD Run CI/CD d: docs/ flutter/flutter/docs, for contributors platform-android Android applications specifically tool Affects the "flutter" command-line tool. See also t: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants