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

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:

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

After:

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:

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:

    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:

    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).
  • 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:

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