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. The page MUST be published at https://docs.flutter.dev/release/breaking-changes/android-agp-new-dsl — that URL is hard-coded as kNewDslBreakingChangeDocsUrl in packages/flutter_tools/lib/src/android/gradle_errors.dart and is printed by the legacy-variant-API error handler and the opt-out removal migrator. Contributor-facing details live in 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 ), which means one more (smaller) gradle.properties change later.

Migration guide

Renaming APKs (applicationVariants.all)

Before:

android {
    applicationVariants.all { variant ->
        variant.outputs.all { output ->
            outputFileName = "myapp-${variant.versionName}.apk"
        }
    }
}

After (Variant API, build.gradle / build.gradle.kts):

androidComponents {
    onVariants(selector().all()) { variant ->
        variant.outputs.forEach { output ->
            // Use variant.name / output.filters and your own naming scheme.
        }
    }
}

For output file renames, prefer consuming the built APKs from SingleArtifact.APK with a task wired through variant.artifacts.use(...), or copy/rename in a finalizer task. Flutter's own copy step already places APKs at build/app/outputs/flutter-apk/app[-abi][-flavor]-<mode>.apk with unchanged names and paths.

Setting per-ABI or per-variant versionCode

Before:

android.applicationVariants.all { variant ->
    variant.outputs.each { output ->
        output.versionCodeOverride = abiCodes.get(output.getFilter(OutputFile.ABI)) * 1000 + variant.versionCode
    }
}

After:

androidComponents {
    onVariants(selector().all()) { variant ->
        variant.outputs.forEach { output ->
            val abi = output.filters.find { it.filterType == FilterConfiguration.FilterType.ABI }?.identifier
            val base = output.versionCode.get() ?: 1
            output.versionCode.set((abiCodes[abi] ?: 0) * 1000 + base)
        }
    }
}

Note: Flutter itself sets per-ABI version codes for --split-per-abi inside onVariants. If your CI mutates version codes in afterEvaluate, that runs at a different time than before; Flutter prints a warning when it detects a divergence between the DSL value and the final output value.

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, for example:

    android {
        buildTypes {
            create("staging") {
                initWith(getByName("debug"))
                matchingFallbacks += listOf("debug", "release")
            }
        }
    }
    
  • Library (plugin) projects cannot be marked debuggable through the public API, so a plugin's BuildConfig.DEBUG and native (JNI) debuggability can differ from before for custom debuggable build types. Variant matching still works via matchingFallbacks.

Add-to-app (Flutter module in a host app)

  • Flutter no longer looks up or configures the host :app project from the module. The dependency between your host‘s asset merging and Flutter’s asset copy is expressed through the Variant API instead of an explicit merge<Variant>Assets.dependsOn(...) edge. Build scripts that reference Flutter's copyFlutterAssets<Variant> tasks by name or type may break: the tasks are now registered lazily and are no longer of type org.gradle.api.tasks.Copy.

  • flutter.hostAppProjectName in gradle.properties is now a no-op. Flutter prints a deprecation warning naming the removal milestone. It was only used for the host-project lookup, which no longer exists.

  • Flutter maps host build types to Flutter build modes using the public “debuggable” flag: profile stays profile, debuggable build types map to debug, everything else maps to release. If your host has no profile build type, add matchingFallbacks:

    create("staging") {
        initWith(getByName("debug"))
        isDebuggable = true            // staging gets debug Flutter artifacts
        matchingFallbacks += listOf("debug", "release")
    }
    

Flutter plugin authors

  • Do not read android.applicationVariants / android.libraryVariants in plugin build scripts; use androidComponents.onVariants.
  • Do not assume Flutter's tasks exist at configuration time or have specific types; look tasks up 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

Variant enumeration for AAR builds now uses the public components API. If your module's build script declares singleVariant(...) publishing itself, Flutter detects the overlap and reports it with an actionable error instead of failing inside AGP.

Escape hatch (temporary)

If you cannot migrate immediately, add the opt-out by hand to android/gradle.properties:

android.newDsl=false

This stops working with AGP 10 (removal of the legacy APIs). Treat it as a short-term unblock only; hand-added opt-outs are never touched by Flutter's migrator.

Timeline

Landed in version: TBD
In stable release: TBD

References