blob: 1c2277db4955396c0f009fa4db3fe0a12006c88b [file] [view]
# Android builds use the new Android Gradle Plugin DSL and Variant APIs
*Draft breaking-change page for `docs.flutter.dev/release/breaking-changes/`.
This file is the source of truth until the page is published to
flutter/website; publishing must complete before the newDsl flip reaches the
beta channel. Contributor-facing details live in
[Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md](Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md).*
## Summary
The Flutter Gradle Plugin now uses only the public Android Gradle Plugin (AGP)
API, and new and migrated Flutter projects build with AGP's new DSL enabled
(`android.newDsl` is no longer set to `false` by Flutter). Gradle build
scripts that use the legacy AGP APIs — most commonly
`android.applicationVariants` — fail to configure and must be migrated to the
AGP Variant API.
## Background
AGP 9 deprecated the legacy DSL and Variant APIs behind the
`android.newDsl=false` flag. AGP 10 removes them entirely. Flutter previously
added `android.newDsl=false` to your `gradle.properties` (via the project
templates and an automatic migration) to keep legacy builds working. That
opt-out stops working with AGP 10, so Flutter has migrated its own Gradle
plugin to the public API and removed the opt-out from templates. A migration
now *removes* the opt-out lines that Flutter previously added — it only touches
lines carrying Flutter's marker comments, and prints a message when it does.
Opt-outs you added by hand are left alone.
`android.builtInKotlin=false` is **not** affected by this change. It is owned
by the separate built-in-Kotlin migration (tracked in
[flutter/flutter#184836](https://github.com/flutter/flutter/issues/184836)),
which means one more (smaller) `gradle.properties` change later.
## Migration guide
### Renaming APKs (`applicationVariants.all`)
Before:
```groovy
android {
applicationVariants.all { variant ->
variant.outputs.all { output ->
outputFileName = "myapp-${variant.versionName}.apk"
}
}
}
```
After (Variant API, `build.gradle` / `build.gradle.kts`):
```kotlin
// Note: androidComponents is a top-level block, peer to android {}
androidComponents {
onVariants(selector().all()) { variant ->
variant.outputs.forEach { output ->
// WARNING: VariantOutput in the new API does not have an outputFileName property.
// You cannot mutate the filename here.
}
}
}
```
For output *file* renames, you must copy or rename the built APKs using a Gradle task.
(Note: Flutter's own copy step already places APKs at `build/app/outputs/flutter-apk/app[-abi][-flavor]-<mode>.apk` with unchanged names and paths.)
Example of a simple finalizer task in `build.gradle` (Groovy):
```groovy
androidComponents {
onVariants(selector().all()) { variant ->
def copyTask = tasks.register("copy${variant.name.capitalize()}Apk", Copy) {
from(variant.artifacts.get(com.android.build.api.artifact.SingleArtifact.APK.INSTANCE))
into(layout.buildDirectory.dir("custom-outputs"))
rename { String fileName -> fileName.replace("app", "myapp") }
}
tasks.matching { it.name == "assemble${variant.name.capitalize()}" }.configureEach {
dependsOn(copyTask)
}
}
}
```
Flutter's copy step reads the APK artifact (`SingleArtifact.APK`) and its
`output-metadata.json` file. If a plugin or your build script transforms that
artifact, the transform must keep the metadata and the APKs' ABI filters:
- Use `toTransformMany(SingleArtifact.APK)` with
`ArtifactTransformationRequest`, which writes the metadata for you, or write
it with `BuiltArtifacts.save()` from a `toTransform` task.
- Produce one APK for each variant output, with the same ABI filter.
Otherwise the build fails with a Flutter error that names `SingleArtifact.APK`.
Fix the transform, or contact the maintainer of the plugin that does it.
Plugins that only read the APK, or that write extra APKs elsewhere after
`assemble`, are not affected.
### Setting per-ABI or per-variant versionCode
Before:
```groovy
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
output.versionCodeOverride = abiCodes.get(output.getFilter(OutputFile.ABI)) * 1000 + variant.versionCode
}
}
```
After:
```kotlin
import com.android.build.api.variant.FilterConfiguration
androidComponents {
onVariants(selector().all()) { variant ->
variant.outputs.forEach { output ->
val abi = output.filters.find { it.filterType == FilterConfiguration.FilterType.ABI }?.identifier
output.versionCode.set((abiCodes[abi] ?: 0) * 1000 + flutter.versionCode)
}
}
}
```
Compute the value from a base you already know rather than by reading
`output.versionCode`. AGP disallows reading `output.versionCode` during
configuration when its compatibility mode
(`android.compatibility.enableLegacyApi`) is off. `flutter.versionCode` is the
value Flutter's templates set in `defaultConfig`. If your product flavors set
their own `versionCode`, use the flavor's value for those variants.
Note: For `--split-per-abi` builds, Flutter sets per-ABI version codes
(`abiOffset * 1000 + versionCode`, with offsets 1 for `armeabi-v7a`, 2 for
`arm64-v8a`, and 4 for `x86_64`) in its own `onVariants` callback. It takes
`versionCode` from your `android {}` block (`defaultConfig` or a product
flavor). A versionCode set only in `AndroidManifest.xml` is not offset.
When the Flutter plugin is applied in the `plugins {}` block, as in Flutter's
templates, that callback runs before an `androidComponents.onVariants` block
in your app's build script, and a value your block sets replaces Flutter's.
Flutter releases that set `versionCodeOverride` applied the offset after your
block, so if your block derives `versionCode` from Flutter's value, the result
differs from those releases. For example, multiplying by 10000 with
`--build-number 42` gives `arm64-v8a` the versionCode `20420000`, where those
releases gave `422000`. To keep the `422000`-style numbers, set the value from
`flutter.versionCode` instead:
```kotlin
import com.android.build.api.variant.FilterConfiguration
val flutterAbiOffsets = mapOf("armeabi-v7a" to 1, "arm64-v8a" to 2, "x86_64" to 4)
androidComponents {
onVariants(selector().all()) { variant ->
variant.outputs.forEach { output ->
val abi = output.filters.find { it.filterType == FilterConfiguration.FilterType.ABI }?.identifier
val offset = flutterAbiOffsets[abi] ?: 0
output.versionCode.set(offset * 1000 + flutter.versionCode * 10000)
}
}
}
```
To verify the result, inspect the built APK:
`apkanalyzer manifest print versionCode build/app/outputs/flutter-apk/app-arm64-v8a-release.apk`
### Custom build types and plugins
Flutter copies your app's custom build types onto Flutter plugin projects so
they resolve. With the new DSL these are `initWith` copies rather than live
aliases:
- Set `matchingFallbacks` on custom build types so dependent Android libraries
resolve. If a third-party plugin does not define your app's custom `staging`
build type, AGP needs to know to safely fall back to compiling the plugin's
`debug` variant rather than failing the build. For example:
```kotlin
android {
buildTypes {
create("staging") {
initWith(getByName("debug"))
matchingFallbacks += listOf("debug", "release")
}
}
}
```
- **Warning for Plugin Authors / JNI Developers:** Library (plugin) projects cannot
be explicitly marked debuggable through the public AGP API. If an app uses a
custom build type (like `staging`), the plugin's `BuildConfig.DEBUG` and native (C++/JNI)
code may silently compile in release mode instead of debug mode. Variant matching
still works via `matchingFallbacks`, but C++ debugging will be broken for those
custom build types.
### Add-to-app (Flutter module in a host app)
- The Flutter module adds its assets and native libraries to its own variants
through the Variant API. Your host app consumes them like the assets of any
Android library. Flutter does not look up or configure the host `:app`
project, and adds no `merge<Variant>Assets.dependsOn(...)` edge to it.
- `flutter.hostAppProjectName` in `gradle.properties` has no effect. If it is
set, Flutter prints a warning that says so. You can remove it.
- The Flutter module's `copyFlutterAssets<Variant>` tasks are of type
`CopyFlutterAssetsTask`, not `org.gradle.api.tasks.Copy`. Build scripts that
look them up with the `Copy` type fail.
- The Flutter build mode comes from the module variant your host build
consumes: the module's `debug` variant builds debug Flutter artifacts,
`profile` builds profile, and `release` builds release. Host `debug`,
`profile` and `release` build types consume the module variant of the same
name. For a custom host build type, AGP picks the module variant through the
build type's `matchingFallbacks`, so list the module build type you want
first:
```kotlin
create("staging") {
initWith(getByName("debug"))
matchingFallbacks += listOf("debug", "release") // staging gets debug Flutter artifacts
}
```
### Flutter plugin authors
- Do not read `android.applicationVariants` / `android.libraryVariants` in
plugin build scripts; use `androidComponents.onVariants`. (We are tracking
an audit of the top 200 plugins for this legacy usage in
[flutter/flutter#190845](https://github.com/flutter/flutter/issues/190845)).
- Do not assume Flutter's tasks exist at configuration time or have specific
types; look up tasks lazily (`tasks.named`) without a type, or better, wire
through Variant API artifacts.
- Test your plugin's example app with AGP 9+ **without** `android.newDsl=false`.
### `flutter build aar`
- If your module's or a plugin's build file declares
`android.publishing.singleVariant(...)` for a variant, Flutter uses that
declaration for that variant and declares publishing (with sources and javadoc
jars) for the other variants.
- `flutter build aar` builds one variant for each build mode, named from
`--flavor` and the build mode (for example, `demoRelease`). The module and
each Android plugin that it uses must have a variant with that name, or the
build fails. Pass `--flavor` for a module with product flavors.
## Escape hatch (temporary)
If you cannot migrate immediately, add the opt-out by hand to
`android/gradle.properties`:
```properties
android.newDsl=false
```
**AGP 10 is expected to remove the legacy APIs**, meaning this opt-out will
stop working. Treat it as a short-term unblock only; hand-added opt-outs are
never touched by Flutter's migrator.
## References
- AGP 9 release notes (new DSL):
https://developer.android.com/build/releases/agp-9-0-0-release-notes
- Flutter umbrella issues:
[flutter/flutter#180137](https://github.com/flutter/flutter/issues/180137),
[flutter/flutter#166550](https://github.com/flutter/flutter/issues/166550)