跪拜 Guibai
← Back to the summary

Keep Flutter Plugins Alive on Old and New AGP Without Breaking Changes

Welcome to follow the WeChat official account: FSA Full Stack Action 👋

I. Overview

Friends who have recently upgraded to Flutter 3.44.x and above have probably been spammed by this prominent warning in the console when compiling projects:

WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin (KGP)... Future versions of Flutter will fail to build if your app uses plugins that apply KGP.

The dreaded KGP (Kotlin Gradle Plugin) is about to be "abandoned"!

It turns out that starting from Android Gradle Plugin (AGP) 9.0, Kotlin support has been completely incorporated as "Built-in Kotlin." Any plugin that unilaterally declares apply plugin: 'kotlin-android' will directly cause a build crash in future Flutter compilation environments.

The official guide (Built-in Kotlin migration for plugin authors) provides a very direct path: just raise the plugin's minimum Flutter version limit to 3.44 in one go, then delete apply plugin: 'kotlin-android', and you're done!

However, in the maintenance of actual open-source components, if we as maintainers forcibly raise the minimum version to Flutter 3.44, it's equivalent to "burning bridges" with users who are still sticking to early versions of Flutter 3.x (or even Flutter 3.0)! Therefore, this guide will take you through a set of adaptation secrets for bidirectional compatibility with old and new Flutter versions.

II. Comparison of Old and New Versions

Before we start, let's use a table to intuitively compare the mechanism differences between the old and new compilation environments. Know yourself and know your enemy, and you can "prescribe the right medicine":

Dimension Old Flutter Build System (< 3.44) New Flutter Build System (>= 3.44 / AGP 9+)
Kotlin Loading Mechanism Relies on plugins explicitly activating via kotlin-android Handled internally by AGP 9.0+; manual apply by plugins conflicts with built-in logic
Kotlin Plugin Configuration Must introduce KGP dependency in buildscript.dependencies Handed over to the main App project or managed by the Flutter build chain; no need to re-declare in sub-plugins
JVM Target Setting Must be specified inside android via kotlinOptions {} Introduces the new outer-level kotlin { compilerOptions {} } DSL

III. Dynamic Compatibility Solution

Here, taking the chat_bottom_container plugin we maintain as an example, to allow the plugin to compile normally in old Flutter (AGP < 9.0) environments, while not causing KGP conflicts under the new Built-in Kotlin (AGP >= 9.0), we need to adopt a "dynamic sniffing" strategy in chat_bottom_container/android/build.gradle.

1. Dynamically Apply the kotlin-android Plugin

We decide whether to manually apply the plugin by parsing the actual AGP version number and checking if the host project has enabled Built-in Kotlin:

// 1. Get the major version number of the Android Gradle plugin in the current build environment
def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize(".")[0] as int

// 2. Determine if Built-in Kotlin is currently active (requires AGP version >= 9 and not explicitly disabled)
def builtInKotlinActive = agpMajor >= 9 &&
    (!project.hasProperty("android.builtInKotlin") ||
        Boolean.parseBoolean(project.property("android.builtInKotlin").toString()))

// 3. Only manually apply the old kotlin-android plugin in old build environments where Built-in Kotlin is not active
if (!builtInKotlinActive) {
    apply plugin: "kotlin-" + "android"
}
  1. During the Gradle configuration phase, get the current AGP major version number (e.g., 8 or 9) from com.android.Version.
  2. Combine agpMajor and the android.builtInKotlin property to calculate the builtInKotlinActive boolean value, used to determine if Built-in Kotlin is currently active.
  3. If the host project still hasn't enabled Built-in Kotlin, we apply a "patch" by manually executing apply plugin via dynamic string concatenation, maintaining backward compatibility while avoiding misjudgment by Flutter.

Q: Why not directly use apply plugin: "kotlin-android"?

A: This is to avoid false positives caused by the limitations of Flutter CLI's static regex scanning mechanism.

When building an application, the Flutter build tool (flutter_tools) does not actually parse and execute Groovy's if-else conditional branches. It simply and crudely uses static regular expressions (RegExp) to scan the build.gradle text of dependent plugins. As long as the literal apply plugin: "kotlin-android" or id "org.jetbrains.kotlin.android" appears in the file, regardless of whether this code is inside a disabled branch, it will be judged as "manually applied KGP," thus throwing a warning.

2. Dynamically Configure JVM Target Version (jvmTarget)

Due to different Kotlin plugin versions, there are two sets of DSL syntax for setting the JVM target. If you directly write the new syntax, old projects will fail to compile under old KGP versions (property not found); write the old syntax, and it will throw deprecation or unsupported errors when compiling in the new environment.

Therefore, we need to adopt the following dynamic property sniffing method:

// 1. Get the kotlin extension configuration object from the project
def kotlinExt = project.extensions.findByName("kotlin")

// 2. Check if this extension object supports the compilerOptions property
if (kotlinExt?.hasProperty("compilerOptions")) {
    kotlin {
        compilerOptions {
            jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8
        }
    }
} else {
    // 3. Fallback to using the traditional kotlinOptions configuration method to prevent errors in old Kotlin Gradle plugins
    android.kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }
}
  1. Safely get the Kotlin extension object via extensions.findByName("kotlin") to prevent null pointer exceptions caused by the project not having Kotlin introduced.
  2. If this extension contains the new compilerOptions, it indicates the current environment is new KGP (1.9+) or Built-in Kotlin. In this case, directly use the new compilerOptions syntax to configure the JVM target.
  3. If it's an old version, fallback to using the traditional android.kotlinOptions { jvmTarget = '1.8' } syntax to ensure smooth compilation of old projects.

IV. Conclusion

Through the above two Gradle dynamic determinations and clever literal concatenation, the plugin can smoothly be compatible with both old and new versions of Flutter and Kotlin compilation environments without making any Breaking Changes.

For users, simply introduce the modified plugin directly; there is no need to forcibly add palliative avoidance configurations like android.builtInKotlin=false in the main project's gradle.properties.

I hope this secret manual helps you friends step on fewer compilation pitfalls. See you next time!

If the article was helpful to you, please don't hesitate to click and follow my WeChat official account: FSA Full Stack Action, which will be the greatest encouragement to me. The official account has not only Android technology, but also articles on iOS, Python, etc., possibly covering skill points you might want to know~