首页
/ Flutter Android APK 的 Gradle 构建全链路:pub 插件解析、子项目编译与 AAR 回退机制

Flutter Android APK 的 Gradle 构建全链路:pub 插件解析、子项目编译与 AAR 回退机制

2026-09-06 18:58:59作者:彭桢灵Jeremy

说明:本文基于当前仓库 docs/platforms/android/How-Flutter-apps-are-compiled-with-Gradle-for-Android.md 一文展开,并结合本仓库 Flutter 框架与 flutter_tools 的源码实现,梳理“Flutter 工具拉取插件 → 写入工程元数据 → 调用 Gradle → 以子项目或 AAR 形式编译插件 → 产出根 APK”的完整链路,供你在排查 Android 构建失败、理解插件编译模型或参与 Flutter Gradle 插件开发时参考。

Flutter 构建 APK 的背后是一条很长的调用链:表面上是 flutter build apk 一条命令,实际会依次经过 Dart 侧的依赖解析、元数据生成,最后“委托”给 Gradle 完成 Android 打包。这条链路分散在 Flutter 框架、Flutter 应用模板、Dart 脚本与 Gradle 脚本多处,且 Gradle 构建是高度异步的,各步骤的执行顺序并不直观,因此非常有必要建立一张“全局地图”。

阅读本文后,你将掌握:Flutter 构建 APK 的端到端执行顺序;插件默认以 Gradle 子项目(subproject) 形式参与源码编译的原因与实现方式;何时会回退到 AAR 二进制依赖 方案;以及出现“插件与主工程冲突、Jetifier/AndroidX 不兼容”等典型错误时如何定位。

一、总体流程:一切最终都委托给 Gradle

先给出全局地图。原文用“项目符号(bullet)”而非“编号列表”来列这些步骤,这本身就是一个重要提示——Gradle 会在看似随机的并行时间点触发其中很多步骤,执行顺序并不严格等于下文书写顺序:

  • 第一步:拉取插件依赖。 Flutter 工具从 pub 拉取 Flutter 应用所依赖的全部插件,并下载到 pub 缓存。该步骤遵循常规 pub get 行为与版本求解(version solving)规则。
  • 第二步:写入工程元数据。 Flutter 工具把工程元数据写入本地临时文件,其中包含本地 Flutter SDK 的位置,以及新下载插件在磁盘上的路径。
  • 第三步:调用 Gradle。 Flutter 工具调用 Gradle 构建 APK。
  • 第四步:读取 build.gradle Gradle 读取应用的 build.gradle,该文件会应用 Flutter 框架提供的 Gradle 插件。
  • 第五步:添加引擎依赖。 Flutter Gradle 插件把 Flutter engine 及其所有 support library 依赖以 jar 依赖形式加入 Flutter 应用。
  • 第六步:添加插件子项目。 Flutter Gradle 插件把应用的全部插件添加为 “subproject”(子项目)Gradle 依赖(通常是默认路径)。
  • 第七步:产出根 APK。 Gradle 把 Flutter 应用及其全部子项目(即 Flutter 插件)一起编译成根 APK。

1.1 Dart 侧与 Gradle 侧的分工

从源码结构看,这条链路被清楚地分成“Dart 工具侧”和“Gradle 构建侧”两段:

1.2 两个“隐藏”的衔接文件:插件清单与 SDK 位置

Dart 工具写入的“工程元数据”在现代 Flutter 工程中至少包含两个关键文件,它们是 Dart 侧与 Gradle 侧之间的“协议”:

  1. .flutter-plugins-dependencies:一份 JSON 插件清单。当前实现由 _writeFlutterPluginsList 写出,flutter_plugins.dart 中记录了它的结构:顶层含 infoplugins(按 ios/android/macos/linux/windows/web 平台分类的插件列表)、dependencyGraph(插件依赖图,为向后兼容保留)、date_createdversion 等字段。源码注释还明确提示:这是生成文件,不要编辑,也不要提交到版本控制。
  2. local.properties:含 flutter.sdk 属性,用于把本地 Flutter SDK 路径交给 Gradle。

需要留意的是,原文档描述的 .flutter-plugins 纯文本文件(记录插件名与磁盘路径)属于早期格式;当前 flutter_plugins.dart 仍在 _writeFlutterPluginsList 逻辑附近保留了对它的处理,但新一代工程以 .flutter-plugins-dependencies JSON 为准。这一点在排查“插件没有参与构建”问题时很关键——检查工程根目录下是否存在该 JSON,即可确认 Dart 侧是否成功解析并写出了插件清单。

二、Flutter Gradle 插件是如何被“应用”的:声明式时代的新结构

原文写于 Flutter Gradle 插件仍以 Groovy 脚本(flutter.gradle)形式提供、应用通过 apply 命令式引入的年代。而当前仓库中的实现已经完成了向 Kotlin + 声明式 plugins 块 的迁移,这是阅读本文时最需要同步更新的背景:

2.1 新工程模板里的接入方式

以仓库自带示例 examples/hello_world/android/settings.gradle.kts 及应用模板 packages/flutter_tools/templates/app/android.tmpl/settings.gradle.kts.tmpl 为例,新工程的 settings.gradle.kts 做了三件事:

  1. pluginManagement 中读取 local.properties 里的 flutter.sdk,并通过 includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 把 Flutter Gradle 插件作为一个 included build 引入;
  2. plugins 块声明三个插件:dev.flutter.flutter-plugin-loader(版本 1.0.0,负责把 flutter 工程与各插件纳入 Gradle 构建)、com.android.application(AGP,apply false)与 org.jetbrains.kotlin.androidapply false);
  3. 通过 include(":app") 声明主工程为 :app 子项目。

注意 examples/hello_world/android/settings.gradle.kts 头部注明“This file is auto generated.”,即这些 Gradle 脚本由仓库内工具(dev/tools/bin/generate_gradle_lockfiles.dart)生成,锁文件(buildscript-gradle.lockfileproject-app.lockfile)也随之维护。

2.2 引擎依赖与仓库解析

FlutterPlugin.apply 中,插件通过 flutter.sdklocal.propertiesFLUTTER_ROOT 环境变量)定位 SDK,并读取 bin/cache/engine.stamp 拼出形如 1.0.0-$engineStamp 的版本号;随后把 Flutter engine 的 Maven 仓库加入 rootProject.allprojects(默认走 FLUTTER_STORAGE_BASE_URLdownload.flutter.io),为后续添加引擎与 embedding 依赖做好准备。这与原文“flutter.gradle 把 engine 及 support library 依赖以 jar 形式加入应用”的职责一一对应,只是实现载体从 Groovy 脚本换成了 FlutterPlugin.kt 里的 Kotlin 代码。

三、默认路径:插件以“子项目”形式参与编译

Flutter 插件的 Android 代码默认不作为预编译二进制引入,而是作为源码交给 Gradle 一起编译,这与 Flutter 应用自身代码的处理方式一致。

实现这一点的技术基础是 Gradle 的“子项目(subprojects)”机制——一种把构建任务组织成互相依赖结构的方式。每个 Flutter 应用工程中:应用自身是一个子项目(:app),每个插件的 Android 代码是另一个子项目。

3.1 实证:./gradlew projects 的输出长什么样

原文给出了一个直观的验证方法:假设某个 Flutter 应用使用了插件 foobar,且 foo 又依赖 bazbaz 作为传递依赖在运行时被打包)。在应用 android 目录下先执行一次 flutter build apk(让 Flutter 工具的 Dart 代码有机会完成构建准备、写入插件元数据),再执行:

$ ./gradlew projects

> Task :projects

------------------------------------------------------------
Root project
------------------------------------------------------------

Root project 'android'
+--- Project ':app'
+--- Project ':bar'
+--- Project ':baz''
\--- Project ':foo'

你可以用同样的方式在自己工程的 android/ 目录复现:foobar 各自成为独立子项目,连传递依赖 baz 也会作为 :baz 子项目出现在列表里——这正是“插件作为子项目源码编译”的直接证据。

3.2 The how:这三方是如何配合的

把插件变成子项目,需要 Dart 工具、Gradle 插件与工程模板三方协作,缺一不可:

  1. Dart 工具写清单:Flutter 工具(flutter_tools)的 Dart 代码在解析完依赖后,把应用使用的全部插件及其磁盘位置写成清单文件(早期为 .flutter-plugins,现代为 .flutter-plugins-dependencies JSON)。入口逻辑见 flutter_plugins.dart,其中每个 Android 插件条目记录 namepathdependenciesnative_build 等信息。
  2. Flutter Gradle 插件读清单并注册子项目:插件读取上述清单后,把每个插件实际注册成 Gradle 子项目依赖。在现代实现里,这是由 packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.ktpackages/flutter_tools/gradle/src/main/kotlin/plugins/PluginHandler.kt 承担的(源码目录还包括 FlutterAppPluginLoaderPlugin.kt 等配套加载器)。
  3. 工程模板引用子项目:Flutter 应用模板中必须有对应的 Gradle 配置把上面注册出来的子项目“引用并构建”起来——在现代工程中这就是 settings.gradle.kts 里的 include(":app")dev.flutter.flutter-plugin-loader,可参见 examples/hello_world/android/settings.gradle.kts 与模板 packages/flutter_tools/templates/app/android.tmpl/settings.gradle.kts.tmpl

3.3 特殊注意事项:源码级插件编译并非 Gradle 的“舒适区”

把插件依赖作为源码引入再编译,并不是 Gradle 一般预期的应用模式。Gradle 围绕依赖管理构建的工具链是为典型 Android 模式(以 .aar.jar 二进制依赖接入)设计的,用在 Flutter 这种“子项目源码依赖”上经常出问题。原文总结了实践中踩过的几类坑,至今仍有很强的排障参考价值:

  • 插件间 build 脚本冲突:每个插件都有自己的 build.gradle,可能与主应用或其他插件发生意外冲突。Gradle 对这类冲突的处理方式因场景而异,有时甚至是未定义行为。
  • 插件出错时堆栈误导:如果某个插件的 Android 代码有问题,整个应用会编译失败,而堆栈未必能清晰指向磁盘上该插件的代码位置。
  • 源码/依赖冲突难排查:插件代码若与应用的 Android 源码冲突(例如存在冲突的传递依赖),Gradle 通常给出难以解读的堆栈。
  • afterEvaluate 的隐患:早期实现中,这些子项目是在 flutter.gradleafterEvaluate 钩子里添加的。而 Gradle 社区普遍不鼓励在 afterEvaluate 块里加逻辑,因为它执行得太晚,几乎处于 Gradle 标准管线末端。当前仓库把插件逻辑前移到 Kotlin 插件(flutter-plugin-loader)阶段,正是对这类架构问题的回应。

四、回退路径:把插件先编译成 AAR

由于子项目方案存在上述问题,Flutter 团队曾探索过另一种路径:让插件先各自编译成独立的 AAR,再以二进制依赖方式(Android 依赖的标准形态、Gradle 最熟悉的方式)接入应用。这一改造最终没有成为默认标准,但当检测到构建失败属于子项目方案的典型故障时,仍会作为静默重试的兜底被自动使用。

4.1 AAR 流程的基本链路

该流程与前文默认流程的前四步完全一致,从第五步开始分叉:

  • Flutter 工具从 pub 拉取插件依赖(同 pub get 行为与版本求解)。
  • Flutter 工具写入工程元数据到本地临时文件(含 Flutter SDK 位置、pub 下载的插件路径)。
  • Flutter 工具调用 Gradle 构建 APK。
  • Gradle 读取应用的 build.gradle,应用 Flutter Gradle 插件。
  • Flutter Gradle 插件把 Flutter engine 及其 support library 依赖以 jar 依赖形式加入应用。
  • (分叉点) Flutter Gradle 插件把每个插件单独编译成 AAR,再把所有插件作为 aar 依赖加入 Flutter 应用。
  • Gradle 把 Flutter 应用及其全部二进制依赖编译成根 APK。

4.2 为什么 AAR 方案没能成为标准

原文给出了三个关键阻塞点,它们决定了该方案的适用范围:

  • 依赖已废弃的 Gradle API:实现依赖 TaskInternal.execute(),而该 API 在 Gradle 5 中已被移除且没有替代品。
  • 构建速度明显更慢:相对子项目流程,AAR 化构建“极其缓慢”。
  • 插件生态的“传递依赖渗透”依赖:现有 Flutter 插件大多基于旧构建系统编写,很多情况下靠子项目系统的“传递依赖渗透”才能编译通过。例如不少插件单独以 AAR 编译会失败,因为它们缺少对 androidx.annotation 的显式依赖;另一些插件则故意把自己的传递 jar 依赖暴露出来、由子项目系统在构建期自动并入应用——若以 AAR 形式引入,这些插件就需要在应用的 Gradle 依赖中手工补齐传递依赖,否则会报 “Class not found” 错误。

4.3 什么情况下仍然会用到它

如今它依然作为静默重试保留:当检测到失败大概率来自子项目结构下常见的错误类型时——具体来说,当构建失败很可能由 AndroidX 不兼容引起时——会退回 AAR 构建。原因是 Jetifier 这类 Gradle 工具在子项目结构下无法正常工作,而先把插件建成 AAR 有时恰好能规避这一类错误。

4.4 仓库中的 AAR 相关实现印证

从当前仓库的 flutter_tools Gradle 目录可以看到 AAR 路径的工程化产物依然健在:

  • packages/flutter_tools/gradle/aar_init_script.gradle:在模块(module)或插件工程中初始化 AAR 构建的 init script。它给各子项目应用 maven-publish,把编译产物发布到本地 Maven 仓库(<outputDir>/outputs/repo),并为每个变体创建 assembleAar<Variant> 任务;对插件工程,还会通过 compileOnly 加入 io.flutter:flutter_embedding_release:1.0.0-$engineVersion embedding 依赖(transitive = false),仅用于暴露 io.flutter.plugin.* 相关 API。
  • packages/flutter_tools/gradle/resolve_dependencies.gradle.ktspackages/flutter_tools/gradle/settings_aar.gradle.tmpl:分别承担 AAR 场景下的依赖解析与 settings 模板生成。
  • 它同时说明了一个反直觉的事实:flutter build aar(模块/插件以 Maven 制品发布给原生宿主工程)场景下,为了让模块的 POM 能正确表达对插件制品的依赖,AGP 期望所有 library 子项目都按 Maven 制品发布,因此插件也必须一并走 AAR 化。

五、实战排查建议

结合上述全链路,当 flutter build apk 失败时可按下述顺序排查:

  1. 确认插件清单已生成:检查工程根目录的 .flutter-plugins-dependencies。若缺失或插件列表不完整,问题多半出在 Dart 侧依赖解析(可先 flutter pub get),而不是 Gradle 侧。
  2. 确认 Flutter Gradle 插件正确接入:检查 settings.gradle.kts 是否包含 pluginManagement + includeBuild(.../packages/flutter_tools/gradle)dev.flutter.flutter-plugin-loader;若工程仍在用旧式 apply plugin: 'flutter',会直接命中 flutter.gradle 开头的 GradleException 并提示迁移。
  3. 查看子项目拓扑:在 android/ 目录运行 ./gradlew projects,核对每个插件(含传递依赖插件)是否都被列为一个 :xxx 子项目。
  4. 区分错误类型:若堆栈指向插件与主工程/其他插件的源码或传递依赖冲突,多半是子项目模式下的已知坑(AndroidX/Jetifier 类问题尤其典型);必要时检查是否触发了 AAR 静默重试,以及是否需要在工程 Gradle 依赖中手工补齐插件的传递依赖。

六、延伸阅读

如果想继续深入,仓库中与本主题直接相关、值得对照阅读的资源有:

需要注意的是,本文描述的是“Flutter 应用在常规 Android 平台下如何经 Gradle 打包”的构建模型;AAR 化路径与子项目路径在当前实现中同时存在,分别服务于“常规 APK 构建”与“模块/插件发布/特定失败重试”等不同场景,理解它们的差异,是看懂 Flutter Android 构建日志与错误堆栈的基础。

登录后查看全文
热门项目推荐
相关项目推荐