Flutter Module 工程模板深度解析:目录结构、生成机制与 add-to-app 实战
【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter
本文以 Flutter 官方仓库中的 Module 模板说明文档 packages/flutter_tools/templates/module/README.md 为骨架,结合同目录下的真实模板文件与 Flutter 工具链源码,系统讲解
flutter create --template module生成的嵌入式(add-to-app)Flutter 工程内部被拆分成哪些子模板、每个子模板会写入磁盘的哪个位置、解决什么问题。读完本文,你将掌握 Module 工程的.android/与.ios/隐藏目录结构、ephemeral(临时)与 editable(可编辑)宿主工程的区别,以及如何在 Android/iOS 宿主 App 中通过 Gradle / Xcode 消费 Flutter 视图与产物。
Module 模板在 Flutter 中的地位
在 Flutter 框架仓库中,Module 工程模板位于 packages/flutter_tools/templates/module,它与app、package、plugin等模板并列,是flutter create命令在指定--template module(对应源码中的FlutterTemplateType.module,见 packages/flutter_tools/lib/src/commands/create.dart)时渲染的一组文件集合。
Module 工程面向的是add-to-app场景:即 Flutter 代码不作为独立 App 运行,而是以「库」的形式被既有原生 Android/iOS 宿主工程引用。这一点直接体现在模板说明文档的多个论断上:
- 模板说明称,Android 侧内容会把 Flutter/Dart 代码「包装成一个 Gradle 工程中定义的 Android library」;
- iOS 侧内容则把 Flutter/Dart 代码「包装成可供 Xcode 工程消费」的形态;
common部分则负责为模块工程本身补充pubspec.yaml等 Dart 工程文件。
因此,Module 模板不生成完整的多端 App 骨架,而只关心两种平台(Android 与 iOS)。源码 packages/flutter_tools/lib/src/commands/create.dart 中同样硬编码了这一约束:The module template only supports iOS and Android,且不支持--platforms参数。
模板顶层布局总览
Module 模板在磁盘上被组织为三个顶层目录,模板说明中分别以## common、## android、## ios三节描述:
| 顶层目录 | 模板说明中对应的产物目标 | 作用 |
|---|---|---|
common | Flutter 模块工程根目录 | 补齐 Dart 工程文件(pubspec.yaml等) |
android | .android/或android/ | 生成 Android library 与宿主 App 工程 |
ios | .ios/等隐藏目录 | 生成供 Xcode 消费的库与宿主 App 工程 |
需要特别注意的是,模板说明文档描述的是「每个子模板被渲染到目标工程的什么位置」这一契约,物理模板文件本身则统一存放在 packages/flutter_tools/templates/module 下。理解这一点对定位模板源码很有帮助:例如文档中 android 一节提到的library子模板,在物理磁盘上对应的是 android/library_new_embedding 目录。
下面按common → android → ios的顺序逐一展开。
common:写入模块根目录的 Dart 工程文件
模板说明用一句话概括了common的职责:
Written to root of Flutter application. Adds Dart project files including
pubspec.yaml.
即common目录下的文件会直接渲染到新建 Module 工程的最外层(如my_module/),内容包括(对照 common 下的实际文件):
pubspec.yaml.tmpl:模块工程的 Dart 依赖清单;lib/main.dart.tmpl:模块默认入口 Dart 代码;test/widget_test.dart.tmpl:配套的 Widget 测试;analysis_options.yaml.tmpl:静态分析配置;README.md.tmpl及 IDE 工程文件模板。
pubspec.yaml是这里最有信息量的文件。查看 common/pubspec.yaml.tmpl 可以看到,普通 App 模板之外,它额外声明了一个flutter: module:段,内含三个由模板引擎注入的关键配置:
flutter: uses-material-design: true module: androidX: true androidPackage: {{androidIdentifier}} iosBundleIdentifier: {{iosIdentifier}}模板中的注释对此有明确说明:这三项标识符不应在生成后随意改动,工具链依赖它们来保持「新增/修改资源与插件」时的一致性;同时它们与原生宿主 App 自身的标识符相互独立,二者可以相同也可以完全不同。从源码看,默认值分别取自androidPackage的com.example.<projectName>风格与应用名组合(见 packages/flutter_tools/lib/src/project.dart 中渲染 Android 模板时的androidIdentifier逻辑),iOS 侧同理由iosBundleIdentifier兜底。
android 子模板详解
模板说明中 android 一节包含五个子条目:library、gradle、host_app_common、host_app_ephemeral、host_app_editable。这五者按职责可以分为「库工程」与「宿主工程」两条主线。
library:把 Flutter 包装成 Android Library
模板说明的核心表述:
Written to the
.android/hidden folder. Contents wraps Flutter/Dart code as a Gradle project that defines an Android library. Executing./gradlew flutter:assembleDebugin that folder produces a.aararchive.
library子模板被渲染到模块工程下的.android/隐藏目录(这也是 Flutter 工具自动生成的宿主与库工程目录),它在物理模板上对应 android/library_new_embedding。目录内提供了:
settings.gradle.copy.tmpl:库工程的 Gradle 设置脚本;include_flutter.groovy.copy.tmpl:负责把 Flutter 相关子工程注入 Gradle 构建的辅助脚本;flutter.iml.copy.tmpl:IDE 模块描述;src/main/AndroidManifest.xml.tmpl:库的 Manifest。
看一下渲染后的settings.gradle(模板原文见 android/library_new_embedding/settings.gradle.copy.tmpl):
// Generated file. Do not edit. rootProject.name = 'android_generated' setBinding(new Binding([gradle: this])) evaluate(new File(settingsDir, 'include_flutter.groovy'))第一行注释与最后两行代码揭示了两个关键事实:其一,.android/内是「自动生成」产物,不应手工编辑;其二,include_flutter.groovy通过 Gradle 的动态求值机制把 Flutter 工程作为子工程挂入当前构建。完成渲染后,只要在.android/目录下执行./gradlew flutter:assembleDebug,即可产出可被宿主 App 依赖的.aar归档——这正是 Android 宿主工程消费 Flutter 视图的载体。
gradle:Gradle 样板补丁
模板说明:
Written to
.android/orandroid/. Mixin for adding Gradle boilerplate to Android projects.
gradle子模板是一组「混入式」文件,用于向 Android 工程补充 Gradle 样板(如 gradle.properties.tmpl、settings.gradle.tmpl、AndroidManifest.xml.tmpl等)。它既可落到隐藏的.android/,也可落到可见的android/——具体落在哪里取决于宿主工程是临时生成还是由作者维护(见下文 ephemeral / editable 之分)。源码中_regenerateLibrary()正是把 android/gradle 渲染进.android/临时目录的(见 packages/flutter_tools/lib/src/project.dart)。
host_app_common:单 Activity 宿主壳
模板说明:
Written to either
.android/orandroid/. Contents define a single-Activity, single-View Android host app with a dependency on the.android/Flutterlibrary. Executing./gradlew app:assembleDebugin the target folder produces an.apkarchive. Used with eitherandroid_host_ephemeralorandroid_host_editable.
host_app_common定义了一个「单 Activity、单 View」的最小 Android 宿主应用:它只包含一个指向 Flutter 视图的MainActivity(模板位于 host_app_common/app.tmpl/src/main/java/androidIdentifier/host/MainActivity.java.tmpl),配以基础的AndroidManifest.xml.tmpl、启动背景launch_background.xml、主题styles.xml与启动图标资源,并且依赖上面提到的.android/Flutter库工程。
它在概念上是「公共底座」,本身不决定宿主工程的存放位置。在.android/或可见的android/目录下执行./gradlew app:assembleDebug,即可得到可直接安装运行的.apk,方便在开发阶段用flutter run或原生 Gradle 直接预览模块效果。
host_app_ephemeral 与 host_app_editable:临时的还是可编辑的?
这两者分别对应模板说明中的:
Written to
.android/on top ofandroid_host_common. Combined contents define anephemeral(hidden, auto-generated, under Flutter tooling control) Android host app...
Written to
android/on top ofandroid_host_common. Combined contents define aneditable(visible, one-time generated, under app author control) Android host app...
二者的模板内容几乎一致(都是基于host_app_common叠加一份settings.gradle,分别见 android/host_app_ephemeral/settings.gradle.tmpl 与 android/host_app_editable/settings.gradle.copy.tmpl),区别只在于目标目录与归属权:
| 宿主工程 | 渲染目标 | 归属与生命周期 |
|---|---|---|
host_app_ephemeral | .android/(隐藏目录) | 自动生成、由 Flutter 工具链全权管理,可随时按需重建 |
host_app_editable | android/(项目可见目录) | 一次性生成,随后交由 App 作者维护,工具链不再覆盖 |
ephemeral(临时)与editable(可编辑)这对术语是理解整个 Module 模板目录哲学的关键:工具链总是优先使用隐藏的临时工程来跑通「最小可运行」场景(flutter run、flutter build等都依赖它),而一旦开发者在根目录手动flutter create .生成了可见的android/宿主工程(例如要接自有 Gradle 配置、改原生代码),工具链就会检测到并停止重建临时宿主壳。
这个分支逻辑在源码中有清晰体现。packages/flutter_tools/lib/src/project.dart 中ensureReadyForPlatformSpecificTooling()会先判断是否需要重新生成,然后:
- 始终先重建库工程(渲染
module/android/library_new_embedding与module/android/gradle到.android/); - 仅在可编辑宿主目录(
android/)不存在时,才叠加渲染host_app_common+host_app_ephemeral到.android/。
// Add ephemeral host app, if an editable host app does not already exist. if (!_editableHostAppDirectory.existsSync()) { await _overwriteFromTemplate(/* host_app_common */, ephemeralDirectory); await _overwriteFromTemplate(/* host_app_ephemeral */, ephemeralDirectory); }也就是说:editable 宿主一旦出现,ephemeral 宿主便让位。同时,判定是否需要重建的条件是「.android/是否比模块根目录的pubspec.yaml更旧,或者是否早于工具链版本戳」——因此当你在pubspec.yaml中新增插件依赖后,下一次构建工具链会自动重新生成隐藏工程,这正解释了模板中「Generated file. Do not edit.」的警告。
ios 子模板详解
iOS 侧同样沿袭「库 + 宿主」的划分,模板说明分三节描述:library、host_app_ephemeral、host_app_ephemeral_cocoapods。值得注意的是,iOS 侧只提供 ephemeral 形态的宿主工程,这与 Android 侧同时提供 editable 形态有所差异。
library:供 Xcode 消费的包装层
模板说明:
Written to the
.ios/Flutterhidden folder. Contents wraps Flutter/Dart code for consumption by an Xcode project. iOS host apps can set up a dependency to this contents to consume Flutter views.
library子模板渲染到模块工程的.ios/Flutter隐藏目录。物理文件位于 ios/library/Flutter.tmpl,包含:
podhelper.rb.tmpl:供 CocoaPods 集成的辅助脚本;AppFrameworkInfo.plist:框架元信息;- 配套的
README.md。
与 Android 的.aar思路一致,iOS 侧的目标是让原生宿主工程能够以依赖形式消费.ios/Flutter目录中的内容,从而在自己的 Xcode 工程中嵌入 FlutterViewController。
host_app_ephemeral:无 CocoaPods 的最小 iOS 宿主
模板说明:
Written to
.ios/outside theFlutter/sub-folder. Combined contents define anephemeral(hidden, auto-generated, under Flutter tooling control) iOS host app with a dependency on the.ios/Flutterfolder contents. The host app does not make use of CocoaPods, and is therefore suitable only when the Flutter part declares no plugin dependencies.
host_app_ephemeral渲染到.ios/下、但不进入Flutter/子目录,它提供了一整套完整的 Xcode 宿主工程骨架(ios/host_app_ephemeral):
Runner.tmpl/:含main.m、AppDelegate、SceneDelegate、Info.plist.tmpl、Base.lproj的启动与主 storyboard、Assets.xcassets图标资源等;Runner.xcodeproj.tmpl/与Runner.xcworkspace.tmpl/:Xcode 工程与工作区描述(含共享 scheme);Config.tmpl/:Debug.xcconfig、Release.xcconfig、Flutter.xcconfig等配置。
一个关键的工程约束是:这个最小宿主不依赖 CocoaPods。它适合纯 Flutter 代码、没有声明任何插件依赖的情形;一旦模块引入插件,就需要 CocoaPods 来拉取与注册插件,此时应使用下一个变体。
host_app_ephemeral_cocoapods:带插件支持的必要变体
模板说明:
Written to
.ios/on top ofhost_app_ephemeral. Adds CocoaPods support. Combined contents define an ephemeral host app suitable for when the Flutter part declares plugin dependencies.
host_app_ephemeral_cocoapods是在host_app_ephemeral之上叠加 CocoaPods 支持的补丁层,物理上对应 ios/host_app_ephemeral_cocoapods,其中Podfile.copy.tmpl即注入的 Podfile。
它在什么条件下被启用?看 iOS 侧的再生逻辑(packages/flutter_tools/lib/src/xcode_project.dart 中_regenerateModuleFromTemplateIfNeeded())会更清楚:与 Android 侧如出一辙,工具链先判断.ios/是否早于pubspec.yaml或早于工具链版本戳,若是则重建.ios/Flutter库内容,再在可编辑宿主不存在时渲染host_app_ephemeral,随后用hasPlugins(parent)探测模块是否声明了插件依赖:
// Add ephemeral host app, if a editable host app does not already exist. if (!_editableDirectory.existsSync()) { await _overwriteFromTemplate(/* host_app_ephemeral */, ephemeralModuleDirectory); if (hasPlugins(parent)) { await _overwriteFromTemplate(/* host_app_ephemeral_cocoapods */, ephemeralModuleDirectory); } }据此可以得出可靠结论:模块是否使用 CocoaPods 变体,由 pubspec 依赖中是否存在插件自动决定,开发者无需手工选择。此外从源码可见,Module 的插件注册宿主在 iOS 侧指向.ios/Flutter下的FlutterPluginRegistrant(见 xcode_project.dart 中pluginRegistrantHost的取值),这也解释了为何 CocoaPods 变体对插件模块是必需项。
从模板到工程:工具链如何编排这些子模板
把以上零散子模板串起来的是 Flutter 工具链中的两套「生成/再生成」流程:
首次创建:
flutter create --template module my_module时,create.dart 的_generateModule()会先把module/common渲染到工程根目录;而.android/、.ios/隐藏工程并非在创建时完整生成,而是由后续平台工具链按需补全(这也是它们在.gitignore语义中通常被视为「生成物」的原因)。按需再生:每次调用构建/运行类命令时,工具链会校验隐藏目录的新鲜度。Android 侧见 project.dart 的
ensureReadyForPlatformSpecificTooling()/_regenerateLibrary():删除旧的.android/,重新渲染library_new_embedding与gradle混入,必要时注入 Gradle wrapper;iOS 侧见 xcode_project.dart 的_regenerateModuleFromTemplateIfNeeded(),对.ios/执行同样的重建。
两套流程共同遵守两个判定条件(均能在源码中找到对应实现):
pubspec.yaml的修改时间是否晚于隐藏目录(pubspecChanged);- 工具链自身的版本戳是否更新(
toolingChanged)。
只要二者满足其一,隐藏目录就会被整目录重建,确保原生工程永远与最新 Dart 依赖/插件声明保持一致。
实战工作流建议
基于模板契约,在实际开发中可遵循以下流程使用 Module 工程:
- 创建模块:在宿主工程旁执行
flutter create --template module(可用--org指定组织名),得到包含pubspec.yaml与lib/main.dart的模块根目录,以及由工具链按需生成的.android/、.ios/隐藏目录。 - 更新依赖:编辑
pubspec.yaml增加依赖后,正常执行依赖获取与构建命令即可;.android/、.ios/里的生成文件无需手工同步,工具链会依据时间戳自动重建。 - Android 集成产物:若要在原生 Gradle 工程中消费,可在
.android/下执行./gradlew flutter:assembleDebug得到.aar;开发期预览可在宿主目录执行./gradlew app:assembleDebug得到.apk。 - iOS 集成注意点:只要模块声明了插件,就必须依赖 CocoaPods 变体(工具链会自动处理);若模块不依赖任何插件,才可使用无 CocoaPods 的最小宿主形态,以规避不必要的 Pod 依赖。
- 是否落地 editable 宿主:需要深度定制原生壳(改
AndroidManifest、原生代码、Gradle 配置)时,才应生成可见的android/(或对应 iOS 的可编辑工程)并交出版面给团队维护;仅用于功能验证时,保持 ephemeral 形态即可,避免与工具链的自动再生机制发生冲突。
小结:一份「写给工具链」的工程蓝图
与常见模板说明不同,templates/module/README.md 本质上是一份「面向 Flutter 工具链的渲染地图」:它不教开发者手写工程,而是精确约定每个子模板该落到哪里、解决什么问题、何时被自动重建。抓住library(产物)— host_app_common(公共壳)— ephemeral / editable(两种宿主形态)— cocoapods(插件能力开关)这条主线,再去读 templates/module 下的模板文件与 project.dart、xcode_project.dart 的再生逻辑,就能对 Flutter add-to-app 的完整运作机制建立体系化认识。
【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考