Flutter Gradle Plugin 全面迁移至 AGP 公共 API:从旧 DSL/Variant API 到 gradle-api 的工程实录

发布时间:2026/9/7 10:36:58
Flutter Gradle Plugin 全面迁移至 AGP 公共 API:从旧 DSL/Variant API 到 gradle-api 的工程实录 Flutter Gradle Plugin 全面迁移至 AGP 公共 API从旧 DSL/Variant API 到 gradle-api 的工程实录【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter本篇技术指南以仓库内 Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md 为骨架系统梳理 Flutter Gradle Plugin下称FGP如何摆脱对旧版 Android Gradle PluginAGPDSL、Variant API 及 AGP 内部实现的依赖全面转向com.android.tools.build:gradle-api公开 API 面。读者读完本文将掌握这次迁移的全部背景动因、端到端的分阶段推进计划P0–P10、逐项的旧用法 → 公共替换 API映射表、必须为下游用户打破的兼容性行为清单以及 FGP 各阶段如何用源码与测试在真实仓库中落地验证。一、为什么需要这次迁移AGP 9/10 的兼容性倒计时文档开篇即点明这次迁移的硬性驱动力全部来自 AGP 版本的演进节奏AGP 92026 年 1 月将旧版 DSL 与 Variant API 弃用deprecated仅通过android.newDslfalse这个逃生舱escape hatch暂时保留AGP 102026 年末将彻底删除这些旧 API同时移除对 AGP 内部实现的访问——届时唯一可用的只有gradle-api工件所暴露的公共 API 面。也就是说android.newDslfalse这个由 Flutter 模板与迁移工具为存量项目注入的保命配置在 AGP 10 下将彻底失效。迁移不是锦上添花而是 FGP 在 AGP 10 时代存活的前提。文档同时给出了 FGP 当前迁移前的负债清单即今天 FGP 仍然依赖旧世界的地方编译时依赖完整的com.android.tools.build:gradle工件对应 build.gradle.kts而不是轻量的gradle-api使用旧版 Variant APIapplicationVariants、libraryVariants、variant.outputs、assembleProvider、packageApplicationProvider、versionCodeOverride使用旧版BaseExtension对应FlutterPluginUtils.getLegacyAndroidExtension导入一个内部 DSL 类com.android.build.gradle.internal.dsl.BuildType位于plugins/PluginHandler.kt导入一个内部工具函数com.android.build.gradle.internal.utils.getKotlinAndroidPluginVersion位于VersionFetcher.kt使用旧式动态 Groovy 驱动flutter build aar位于aar_init_script.gradle。作为背景补充Flutter 模板当前把 AGP 钉在 9.1.0 并默认注入android.newDslfalse配套的迁移工具disable_new_dsl_migration.dart负责给已有项目追加这一退出开关。这份 opt-out 能力正是 AGP 10 中会随旧 API 一起消亡的东西。二、迁移的 End State四个可验证的终态目标在动手之前文档先定义了清晰的完成标准End State使整个迁移有明确的验收线API 纯度FGP 只使用公共 API编译目标从完整gradle工件切换到gradle-api模板净化项目模板不再向gradle.properties注入android.newDslfalse工具方向反转原先添加opt-out的迁移器被移除opt-out的新迁移器取代——后者只删除由 Flutter 添加、带标记注释的 opt-out 行全新工程可用一个新建的flutter create应用在newDsl开启on的状态下可以直接构建成功。其中第四点是最直观的验收信号newDsltrue不再需要任何 Flutter 侧的特殊配置就能完成全新项目的全流程构建。三、决策记录Decision Records迁移路上被钉死的六条技术决策迁移过程中不可避免会遇到取舍文档用六条决策记录把关键选择及其理由固化下来供后续实现者遵循最低 AGP 版本门槛min AGP floor不在本次范围。地板升级由独立的版本提升 PR 负责在 master 上已由 PR #176858 与 #177416 将 floor 提升至8.11.1本迁移在此新地板上平滑叠加。本文档中每个替换 API 都已在gradle-api:8.11.1内验证为公共 API通过反编译 jar 检查确认。如果实现过程中真发现某个替换 API 必须依赖更高的最低 AGP 版本需要在此文档中写明是哪个 API、为何没有兼容替代品然后再提升版本门槛——否则本次工作不触碰版本地板。8.x 中是公共 API不等于9.x 上二进制兼容。公共CommonExtension在 AGP 8 与 9 之间发生了破坏性变化这正是 AgpCommonExtensionWrapper.kt 存在的原因。对应的缓解措施是双重的从P2 阶段起强制要求一条 CI/测试轴——让 FGP 针对 gradle-api 9.x 编译同时增加字节码检查javap grep确保编译后的 FGP 类没有任何一个把CommonExtension作为 owner 引用。android.builtInKotlinfalse不在本次范围内。翻转它需要独立的 built-in-Kotlin 迁移工作流。用户可以预期之后还会再迎来一次更小规模的gradle.properties变更breaking-change 页面已明确说明这一点。推论是P9 的移除迁移器必须以android.newDsl属性行为锚点绝不能只依赖标记注释的措辞——因为模板里builtInKotlin的标记注释与它几乎一模一样锚错位置会误删。Per-ABI versionCode 机制不重新发明。不要在finalizeDsl快照上重建 AGP 的 flavor-merge 优先级。首选机制在 P6 中先做 spike 验证在onVariants回调里对VariantOutput.versionCode做先读后写read-then-set——该值已被种入合并后的结果写入abiOffset * 1000 current避免自引用的.map写法。只有当先读后写不可行时才退回到快照方案并把结论记录到本文档中spike 结果当前标注为 pending。buildModeFor的语义收敛。每个 variant 作用域的调用都统一使用(name, debuggable)重载并读取公共的Component.debuggable。基于名称的推断name-based inference仅保留给唯一一个没有公共信号可用的 DSL 作用域场景即PluginHandler中 library 插件的 build-type 复制。这一收敛保住了 add-to-app 场景中自定义 debuggable 的匹配行为——例如宿主项目的staging可调试build type 仍能正确映射到 debug 引擎产物。P3 阶段预研 / PR 4newDsl 下的 afterEvaluate DSL 变更。原计划用一个 scratch 应用做 spikeAGP 9.1 newDsltrue 自定义 build type验证从pluginProject.afterEvaluate创建 build type 是否仍然可用但由于实现沙箱无法访问 AGP 工件而未能运行。initWith复制方案成为主方案落地需要android_plugin_example_app_build集成测试与自定义 build type 的 scratch 构建在 CI 中确认。文档记录的兜底方案是若 newDsl 拒绝afterEvaluate内的变更则改为在插件工程的androidComponents.finalizeDsl中执行复制。值得对照源码阅读当前 AgpCommonExtensionWrapper.kt 的类注释里写有一条CRITICAL约束——不要在文件内 import 或引用com.android.build.api.dsl.CommonExtension否则编译器可能把被破坏的类型织入字节码。它通过when分支把backingExtension: Any分别按ApplicationExtension/LibraryExtension/DynamicFeatureExtension/TestExtension分发到各自类型安全的读写从而绕开 AGP 8→9 之间 CommonExtension 的二进制不兼容是对第 2 条决策最直接的源码印证。四、替换映射表从每一处遗留用法到公共 API这是迁移的核心作战地图。文档用一张表把每一处遗留用法、所在文件、公共替换方案与所属阶段一一对应起来遗留用法位置公共替换方案阶段internal.utils.getKotlinAndroidPluginVersionVersionFetcher.kt删除依靠既有回退链kotlin_version属性 →KotlinAndroidPluginWrapper.pluginVersion→ 反射KGP 缺席时为 null 是可接受结果P1compileSdkVersion字符串比较android-NN子串FlutterPluginUtils.getCompileSdkFromProject、PluginHandler警告wrapper 的compileSdk/compileSdkPreview数值比较并定义清晰的 preview 语义P1BaseExtension.ndkVersionFlutterPluginUtils.getConfiguredNdkVersionwrapper 的ndkVersionP1buildModeFor(BuildType)旧模型类型FlutterPluginUtils.ktbuildModeFor(name, debuggable)重载P2getLegacyAndroidExtension(project).buildTypes循环PluginHandler.ktwrapper 的 new-DSLbuildTypes容器P2internal.dsl.BuildType向插件工程活别名PluginHandler.kt基于 new-DSLBuildType的initWith复制仅当两侧都是ApplicationBuildType时才拷贝 app 专属属性P3BaseExtension/getLegacyAndroidExtension其余调用点FlutterPluginUtils.ktwrapper 访问器含externalNativeBuildP4急切的applicationVariants.configureEach任务创建mergeAssets/processResources 钩子FlutterPlugin.kt、FlutterPluginUtils.kt合并的onVariants块CopyFlutterAssetsTaskvariant.sources.assets.addGeneratedSourceDirectoryP5variant.outputspackageApplicationProviderdoLast拷贝 APKversionCodeOverrideFlutterPluginUtils.ktCopyFlutterApksTaskSingleArtifact.APKBuiltArtifactsLoader对VariantOutput.versionCode先读后写P6libraryVariants.all× 宿主applicationVariants.all交叉接线FlutterPlugin.ktadd-to-applibrary 侧onVariantsComponent.debuggable不再查询宿主工程P7aar_init_script.gradle中的动态 Groovy 旧 APIaar_init_script.gradle基于components的枚举ext-property 守卫P8模板/migrator 中的android.newDslfalse模板、disable_new_dsl_migration.dart从模板删除RemoveNewDslOptOutMigrationP9完整gradle工件依赖build.gradle.ktsgradle-api工件编译期证明零内部使用P10对照当前仓库源码可以看到这场迁移的实况VersionFetcher.kt里已经不存在对 AGP 内部工具的引用其getKGPVersion方法完整实现了文档第 1 条中的回退链——先读kotlin_version工程属性再尝试KotlinAndroidPluginWrapper.pluginVersion最后用反射按pluginVersion/kotlinPluginVersion字段名兜底并在任何一步失败时优雅返回null注释特别说明在 AGP 内置 Kotlin 支持即android.builtInKotlin场景下没有独立 KGP调用方必须把 null 理解为未知/未应用而不是错误对应源码见 VersionFetcher.kt。五、阶段规划表P0–P10 的 PR 粒度拆解文档强调每个阶段phase都是一个单 PR 规模的独立变更跑在自己的分支上。P8 是独立通道Groovy 脚本、文件互不相交P0 与 P1 互不相交其余阶段都串行经过FlutterPlugin.kt/FlutterPluginUtils.kt因而相互之间存在先后依赖。阶段分支名规模摘要P0agp-api-docS本文档 网站 breaking-change 页面草稿P1agp-internal-utilsS移除 VersionFetcher 的内部工具compileSdk 数值比较ndkVersion 走 wrapperP2agp-buildmode-depsMbuildModeFor重载new-DSL 的 Flutter 依赖9.x 编译轴P3agp-plugin-buildtypesM插件 build type 的initWith复制删除内部 import内部 import 的 lintP4agp-ndk-fallbackS删除BaseExtensionexternalNativeBuild 走 wrapperP5agp-assets-onvariantsL懒任务注册5a 生成的资源目录接线5bP6agp-apk-copy-versioncodeLCopyFlutterApksTaskper-ABI versionCodeapp 路径脱离遗留 APIP7agp-add-to-appLlibrary 侧onVariants删除宿主交叉接线与 P5a 的遗留分支P8agp-aar-scriptMaar_init_script 的公共 API 清理P9agp-newdsl-flipM模板移除 opt-out移除型迁移器新增错误处理器P10agp-gradle-apiM依赖切换到gradle-api测试迁移规模约定S/M/L与阶段间的序列化关系意味着任何想合入的 PR 都必须遵守后续 R2 规则的回滚窗口约束不能随意打乱顺序。六、跨切面规则Cross-cutting Rules保证迁移质量与安全网的六条铁律文档用 R1–R6 编号定义了横贯所有阶段的工程纪律R1 锁步Lockstep任何修改 FGP 输出消息文本的 PR必须在同一个 PR 内同步更新 gradle_errors.dart注位于packages/flutter_tools/lib/src/android/中对应的匹配器及其 Dart 测试杜绝错误消息与解析器脱节。R2 回滚备注Revert notes每个 PR 描述里都要写明在 X 阶段落地前可安全回滚一旦被后续阶段取代策略转为 fix-forward向前修复。依赖链上的相邻阶段之间至少要做一次完整的 post-submit CI 浸泡禁止 P2–P4 同日叠加no same-day stacking。R3 9.x 编译轴从 P2 开始gradle 单元测试在 CI 中额外针对 gradle-api 9.x 编译并执行 javapCommonExtension字节码检查呼应决策记录第 2 条。R4 配置缓存Config-cache先在 master 上建立基线每个阶段的断言是不新增config-cache 违规而非追求完整复用。R5 内部 import 的 lintP3 一旦落地一个入库checked-in的测试就禁止src/main中出现com.android.build.gradle.internal.*导入。当前仓库中对应测试可见于 gradle/src/test/kotlin/validation/ 下的InternalAgpApiImportTest.kt与BytecodeValidatorTest.kt。R6 分阶段开启newDsltrue的测试轴app 流程在 P6 结束时转绿add-to-app 从 P7 转绿aar 从 P8 转绿完整的矩阵是P9 的门禁gate。七、回滚窗口表每个阶段的回滚时限由于阶段间存在序列化依赖回滚窗口各不相同文档给出的完整表格如下阶段回滚窗口P0始终可安全回滚P1–P4各自保持到链上下一阶段落地为止此后转为 fix-forwardP5保持到 P6 落地P6 / P7相互容忍app/module 路径互不相交直到 P10P8P10 之后仍可安全回滚但 P9 之后不可P9可独立干净回滚P10可独立干净回滚这张表实际是一条变更保险策略只要每个 PR 知道自己处于哪个回滚窗口内仓库维护者就可以在任何一步出问题时选择回滚或 forward-fix而不至于把整个迁移链一把梭。八、必须为用户打破的兼容性行为十条破坏性变更前瞻迁移期不可避免会改变下游用户可观察的行为。文档专门维护了一张必须打破的功能清单并在实现推进中持续更新。这十条既是 breaking-change 页面website-page-draft.md的内容来源也是各阶段验证的重点用户构建脚本中的遗留 API最典型的如applicationVariants.all的 APK 重命名配方在 newDsl 下会失败——这是影响面最大的一次破坏。缓解手段是 P9 新增的错误处理器 breaking-change 网站页面。flutter-apk 拷贝行为产物的名字/路径保持不变app[-abi][-flavor]-mode.apk与现有拼接顺序字节级一致但原先的doLast块被一个支持 UP-TO-DATE 的 finalizer 任务取代gradlew tasks中会出现新的任务名。Per-ABI versionCodefinalizeDsl之后用户的变更典型如 CI 中afterEvaluate的写法时序可能变化行为可能不同运行时新增一个发散警告。自定义 build type → 插件原本活别名live-alias的实例变成initWith复制library 插件无法接收isDebuggableLibraryBuildType上没有公共 setter。重要警告如果 app 使用自定义 build type如staging插件的BuildConfig.DEBUG与原生C/JNI代码可能静默地以 release 模式而非 debug 模式编译。匹配逻辑通过matchingFallbacks保留但那些自定义 build type 的 C 调试将被破坏。资源合并Flutter 资源从合并后覆写变为合并源目录merged source dir冲突时按 AGP source-set 优先级解析。Add-to-app显式的:app:mergeVAssets.dependsOn边与宿主工程查询被移除flutter.hostAppProjectName变为 no-op 并给出带移除里程碑的弃用警告。按名字引用copyFlutterAssetsV任务的脚本如tasks.getByPath(...)会因任务改为懒注册而崩溃报Task with path ... not found。任务实现/类型Flutter 任务变成懒TaskProvider且copyFlutterAssetsV的类型从org.gradle.api.tasks.Copy换成自定义任务类——tasks.named(..., Copy::class)的强转会失败。flutter build aarsingleVariant 去重守卫改为 ext-property/try-catch 并带指定错误消息variant 枚举从libraryVariants移到components——用户声明部分singleVariant时表现会不同。newDsl 翻转新工程不再拥有 opt-out移除迁移器只删除带标记的android.newDsl行模板标记为 This newDsl flag was added by the Flutter template迁移器标记为 This newDsl flag was added automatically by Flutter migrator以属性行本身为锚绝不触碰相邻的 builtInKotlin 行用户手写的 opt-out 会被保留。compileSdk 不匹配警告改为数值比较并定义 preview-vs-numeric 的明确语义消息保留旧文案的独特子串以便检索。第 9 条中以属性行锚定、绝不误删相邻 builtInKotlin 注释行的设计正是为了精确落实决策记录第 3 条的警告。当前仓库中的旧迁移工具 disable_new_dsl_migration.dart 仍持有android.newDslfalse的旧逻辑DisableNewDslMigration类P9 将把它替换为反向的RemoveNewDslOptOutMigration。九、为下游用户准备的迁移指南要点面向用户的 breaking-change 页面草稿 website-page-draft.md 与本文档并存同为 P0 产物其中给出了三类最常见的迁移配方供受影响的工程直接对照1) 重命名 APKapplicationVariants.all配方旧写法在 newDsl 下会失败新 API 中VariantOutput没有outputFileName属性无法就地改文件名必须用 Gradle 任务拷贝/重命名产物。一个最小 finalizer 任务示例如下build.gradle/ GroovyandroidComponents { 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) } } }2) 设置 per-ABI / per-variant 的 versionCode旧写法依赖output.versionCodeOverride新写法在onVariants内对output.versionCode先读后写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) } } }若 CI 在afterEvaluate里改 versionCode执行时序已与从前不同Flutter 检测到 DSL 值与最终输出值发散时会打印警告。可用apkanalyzer manifest print versionCode build/app/outputs/flutter-apk/app-release.apk直接核对产物中的最终 versionCode。3) 自定义 build type 与插件Flutter 会把 app 的自定义 build type 复制到各 Flutter 插件工程以保证它们能解析newDsl 下这是initWith复制而不再是活别名且受上节第 4 条警告约束library 插件收不到isDebuggable自定义 build type 的原生调试可能静默变 release 模式。此外页面还明确排除了一个极易混淆的点android.builtInKotlinfalse不受本次变更影响它归属独立的 built-in-Kotlin 迁移。十、验证矩阵拿什么证明迁移没做坏文档在末尾给出分层验证策略完整矩阵在P6、P7、P9、P10各执行一次其余阶段做针对性验证。验证动作包括在packages/flutter_tools/gradle下执行./gradlew test外加 R3 的 9.x 编译轴执行各阶段点名的针对性integration.shard测试Scratch-app 全矩阵apk/appbundle × 3 种模式debug/release/profile--flavor--split-per-abi附加 apkanalyzer versionCode 断言包括 flavor 自定义 versionCode 的用例--deferred-components带自定义 build type 的插件flutter build aaradd-to-app 的 source 与 AAR 宿主两种流程flutter run/ hot restart /flutter attachWindows 上对拷贝任务的冒烟测试AGP 轴当前地板版本和9.1 newDslfalse再按 R6 分阶段开启newDsltrue按 R4 检查 config-cache。这套矩阵与真实仓库中的工程化资产一一对应当前 FGP 的 Gradle 单元测试位于 gradle/src/test/kotlin/如FlutterPluginTest.kt、AgpCommonExtensionWrapperTest.kt、plugins 目录下的PluginHandlerTest.kt而 Flutter 工具层的 Dart 侧测试如命令 shard 的 build_aar_test.dart、build_aar_test.dart负责验证flutter build aar、迁移器行为等端到端流程。结语一次教科书式的框架自迁移从更宏观的角度看这份文档是一次大型框架从自身长期依赖的内部 API 迁移到上游公共 API 的完整工程样板它先讲清楚外部时间线压力AGP 9 弃用、AGP 10 删除再列出精确的现状负债清单然后用决策记录封存技术取舍用替换映射表 阶段表 跨切面规则 回滚窗口四件套组织十几个 PR 的执行秩序最后用破坏性变更清单与验证矩阵兜住用户可见风险。无论是关心 Flutter Android 构建链未来走向的普通开发者还是需要参考如何安全地让插件库告别某个上游框架的内部实现这一命题的构建工程师都可以从 Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md 与配套的 website-page-draft.md 中直接受益。【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考