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.

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), 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):

// 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):

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)
        }
    }
}

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.orNull ?: 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. To verify your mutations worked, inspect the built APK: apkanalyzer manifest print versionCode build/app/outputs/flutter-apk/app-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:

    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)

  • 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 will break. For example, tasks.getByPath(":flutter:copyFlutterAssetsDebug") will crash your build with a Task with path ... not found error because 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. (We are tracking an audit of the top 200 plugins for this legacy usage in flutter/flutter#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

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

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