Flutter add-to-app 实战:用纯 Android 宿主应用验证 Flutter Module 的 V2 Embedding 集成
【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter
本篇技术指南围绕 Flutter 仓库中的纯 Android 宿主应用模板 android_host_app_v2_embedding 展开,它是 devicelab 集成测试 用来验证「在已有 Android 应用中嵌入 Flutter Module(add-to-app)」场景的宿主工程。读完后,你将掌握:如何用 V2 Embedding 编写一个只含原生代码的 Flutter 宿主 Activity、如何通过 Maven AAR 依赖把 Flutter Module 产物接入现有 Android 工程,以及 Flutter CI 如何端到端地验证这条 add-to-app 构建链路。
1. 模板的定位:为flutter create -t module准备的宿主应用
根据该目录的 README,这个工程是一个「Android host app」,它的用途明确:
$ flutter create -t module hello即:先用上面的命令创建一个 Flutter Module 工程(hello),把它放在宿主应用的**同级目录(sibling folder)**中,然后由宿主应用作为纯 Android 工程去依赖这个 Module 的构建产物。该模板被 devicelab 任务build_android_host_app_with_module_aar.dart直接引用,用于回归验证「已有 Android App + Flutter Module」这条 add-to-app 构建路径。
所属目录 pure_android_host_apps 的整体定位也印证了这一点——它是「集成测试中用于测试 add-to-app 用例的最小 Android 应用集合」。所谓「纯(pure)」,指宿主工程的源码里没有任何 Flutter 平台代码或 Flutter CLI 参与其构建,Flutter 能力完全以依赖包形式注入,这正是真实 add-to-app 场景的形态。
2. V2 Embedding 的宿主 Activity:整个工程最核心的两行代码
宿主应用唯一的 Java 源文件是 MainActivity.java:
package io.flutter.add2app; import io.flutter.embedding.android.FlutterActivity; public class MainActivity extends FlutterActivity { }这里的关键在于导入的包名是io.flutter.embedding.android.FlutterActivity——这是 FlutterV2 Embedding的 Activity 基类(V1 Embedding 位于io.flutter.app包下)。继承它之后,FlutterActivity会在运行时负责创建FlutterEngine、加载FlutterShellArgs并完成引擎与 Activity 的绑定,所以业务侧不需要写任何引擎生命周期代码。
对应的 AndroidManifest.xml 同样极简,仅注册了这个 Activity:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application android:allowBackup="false" tools:ignore="GoogleAppIndexingWarning,MissingApplicationIcon"> <activity android:name=".MainActivity" /> </application> </manifest>注意这里没有io.flutter.embedding.android.FlutterEmbedding相关的 manifest 配置,也无需声明插件权限——因为 Flutter 侧的配置(引擎、插件、flutterProjectType元数据等)随 Module 的 AAR 产物一起打进最终 APK,由构建期合并。devicelab 测试后续会解包 APK 校验合并后的 manifest 中包含:
<meta-data android:name="flutterProjectType" android:value="module" />这一元数据是 V2 Embedding 识别「当前宿主来自 Flutter Module」的关键标记,见 build_android_host_app_with_module_aar.dart 第 300-308 行 的断言逻辑。
3. 依赖注入方式:用 AAR 坐标引入 Flutter Module 产物
宿主工程与 Flutter Module 的衔接全部发生在 Gradle 依赖声明中。app/build.gradle 的内容如下:
apply plugin: 'com.android.application' android { namespace = "io.flutter.add2app" compileSdk = 36 ndkVersion = "28.2.13676358" // This version must exactly match the version of the NDK that the recipe pulls from CIPD. compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } defaultConfig { applicationId "io.flutter.add2app" minSdk = 24 targetSdk = 36 versionCode 1 versionName "1.0" } buildTypes { profile { initWith debug } } } dependencies { debugImplementation 'io.flutter.devicelab.hello:flutter_debug:1.0' profileImplementation 'io.flutter.devicelab.hello:flutter_profile:1.0' releaseImplementation 'io.flutter.devicelab.hello:flutter_release:1.0' }几个值得注意的实现细节:
- 分构建变体引用不同 AAR:debug / profile / release 三个变体分别依赖
flutter_debug、flutter_profile、flutter_release三个 Maven 坐标(groupIdio.flutter.devicelab.hello)。groupId 中的io.flutter.devicelab来自 CI 创建 Module 时传入的--org io.flutter.devicelab参数(见 任务源码第 64-69 行),即 AAR 的坐标由 Module 工程自己的--org和目录名决定,宿主侧必须严格对齐。 - profile 变体:
buildTypes { profile { initWith debug } }显式声明了 profile 构建类型,保证flutter_profileAAR 也有变体可挂载——这是 Module 模式支持 profile 构建(性能分析)的体现。 - NDK 版本强约束:
ndkVersion = "28.2.13676358"的注释明确要求它必须与 CI 配方(recipe)从 CIPD 拉取的 NDK 版本完全一致,属于 CI 环境的硬约束,本地复现该测试时同样需要满足。 - SDK 基线:
minSdk = 24、targetSdk = 36、compileSdk = 36,Java 17 编译选项,代表当前 CI 所采用的 Android 构建基线。
而 AAR 从哪里来?根 settings.gradle 给出了仓库声明:
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) repositories { google() mavenCentral() def flutterStorageUrl = System.getenv("FLUTTER_STORAGE_BASE_URL") ?: "https://storage.googleapis.com" maven { url = uri("$flutterStorageUrl/download.flutter.io") } maven { url = uri("../hello/build/host/outputs/repo") } // BEGIN ci only configuration for getting engine artifacts in pre-submit environments ... // END ci only configuration for getting engine artifacts in pre-submit environments } } include ':app'../hello/build/host/outputs/repo:相对路径指向同级目录hello(即 Flutter Module 工程)的flutter build apk输出中的本地 Maven 仓库,flutter_debug/profile/release三个 AAR 就是flutter build apk(模块的 ephemeral host 构建)发布在这里的。这解释了 README 中「Module 工程必须放在宿主同级目录」的原因。$flutterStorageUrl/download.flutter.io:Flutter 引擎构件(如libflutter.so对应的引擎 AAR)的标准下载仓库,通过FLUTTER_STORAGE_BASE_URL环境变量可切换镜像。- CI 专用段落:从 Module 工程的
.android/local.properties读取flutter.sdk路径,再拼接bin/cache/engine.realm中的 realm 前缀追加一个 maven 仓库。从源码结构看,这是 pre-submit 环境下引擎构件尚未发布到公共仓库、需要按 realm 从预发布地址取用时的兜底配置,注释也明确标注为「ci only」。
其余配置文件中,gradle.properties 设置了-Xmx8G的 JVM 参数与android.useAndroidX=true(使用 AndroidX 的 V2 Embedding 工程必须开启)。
4.REPLACEME占位符:模板为何故意不带具体版本号
打开 gradle-wrapper.properties 和根 build.gradle,会发现两处版本号被写成了占位符:
# gradle-wrapper.properties distributionUrl=https\://services.gradle.org/distributions/gradle-REPLACEME-bin.zip// build.gradle(根项目) buildscript { repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:REPLACEME' } }这并不是漏写,而是模板的刻意设计:devicelab 任务在运行时用参数化版本原地替换REPLACEME(见 任务源码第 239-253 行:propertyContent.replaceFirst('REPLACEME', gradleVersion)与topBuildContent.replaceFirst('REPLACEME', agpVersion.toString()))。这样可以用同一份宿主模板覆盖多组 Gradle / AGP 版本矩阵。当前 CI 实际运行两组组合(见 任务入口 main()):
| Gradle 版本 | AGP 版本 | 覆盖场景 |
|---|---|---|
| 8.4 | 8.2.1 | AGP 8.3 之前的旧链路 |
| 8.13-rc-1 | 8.8.1 | AGP 8.3 及以后的新链路(含 RC 版本兼容性) |
任务中还针对agpVersion >= 8.3做了差异化断言:AGP 8.3 起合并资源的中间产物目录多了一层mergeDebugAssets/mergeReleaseAssets路径(第 310-328 行),这是 AGP 版本升级对构建中间产物结构的直接影响。
5. Devicelab 集成测试的完整验证链路
模板本身只有几十个文件,它的价值由 build_android_host_app_with_module_aar.dart 定义的端到端流程激活。该测试按以下顺序执行(ModuleTest类,buildTarget = 'module-gradle'):
- 查找 Java:
findJavaHome(),后续所有 Gradle 命令注入JAVA_HOME。 - 创建 Flutter Module:
flutter create --org io.flutter.devicelab --template=module hello,生成与 README 描述一致的 Module 工程。 - 注入 FFI 包与只读资源:创建一个名为
ffi_package的本地 FFI 包加入 Module 依赖,并在assets/下生成read-only.txt(非 Windows 上chmod 444)。这两个注入项用于后续验证原生.so与资源文件访问权限是否正确进入宿主 APK。 - 构建 Flutter Module 库归档:在 Module 的
.android目录执行gradlew flutter:assembleDebug,断言产出.android/Flutter/build/outputs/aar/flutter-debug.aar(第 124-150 行)。 - 构建 ephemeral 宿主 APK:
flutter build apk,检查build/host/outputs/apk/release/app-release.apk存在;接着flutter clean后再构建一次,验证 clean 后可重建(editable 模式)。 - 验证
flutter build aar:flutter build aar --no-profile直接构建 AAR 成功。 - 接入本文的宿主模板:把
dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding递归拷贝到临时目录的hello_host_app,并从 Module 的.android目录拷入gradlew与gradle-wrapper.jar,然后替换REPLACEME为指定 Gradle / AGP 版本(第 217-253 行)。 - 构建宿主 debug APK 并校验内容:
gradlew app:assembleDebug后,用 apk_utils 解包 APK 断言包含flutter_assets关键文件、lib/arm64-v8a/libflutter.so、libapp.so以及lib/arm64-v8a|armeabi-v7a/libffi_package.so(FFI 包的两个 ABI 产物),并校验 manifest 中的flutterProjectType = module元数据。 - 只读资源权限检查:定位中间产物
app/build/intermediates/assets/debug[/mergeDebugAssets]/flutter_assets/assets/read-only.txt,要求文件权限以rw开头——即源文件是 444 只读,进入 APK 后必须对用户可读(否则运行时RootBundle加载会失败),这是对 Gradle merge assets 行为的一次真实回归。 - 构建宿主 release APK 并校验:
gradlew app:assembleRelease后检查app-release-unsigned.apk,断言两个 ABI 的libflutter.so/libapp.so/libffi_package.so齐全,并解包assets/flutter_assets/NOTICES.Z(gzip + utf8 解码后须包含skia与Flutter Authors字样)验证开源许可证文件正确生成。 - 构建过程日志断言:检查 stderr 中不得出现
You are applying Flutter's main Gradle plugin imperatively——即要求宿主工程以声明式(plugins {}/ 依赖方式)而非命令式apply方式接入 Flutter Gradle 插件,防止构建链路退化到旧写法。
任一步失败,任务即返回TaskResult.failure;finally块清理临时目录。测试由combine()将两组版本矩阵串行为一个TaskFunction顺序执行。
6. 对实际 add-to-app 场景的可迁移结论
从这个模板和测试中可以提炼出接入真实项目的几条经验:
- 宿主侧的最小实现就是一个继承
io.flutter.embedding.android.FlutterActivity的 Activity + 一条 Activity 声明,其余 Flutter 配置随 AAR 合并,这大幅降低了原生团队的理解成本。 - Module 与宿主的耦合点是 Maven 坐标与仓库路径:宿主依赖
'<org>.<module名>:flutter_<variant>:1.0'这样的坐标,并需要能访问 Module 发布的本地仓库(build/host/outputs/repo)以及download.flutter.io引擎构件仓库;两者缺一都会导致依赖解析失败。 - 版本矩阵意识:模板用
REPLACEME占位、由 CI 注入 Gradle/AGP 版本的做法,说明 add-to-app 构建链路必须同时对新旧 AGP(尤其 8.3 前后的产物结构差异)保持兼容;自己维护宿主工程时,升级 AGP/Gradle 后应回归「APK 内产物清单 + manifest 元数据 + 资源权限」这三类检查。 - 验证手段可复用:测试中「解包 APK 检查 so 文件与资源权限、解包 NOTICES.Z 检查许可内容、校验
flutterProjectType元数据」这些手法,同样适用于日常排查「为什么我的宿主 APK 里缺 flutter 产物」这类问题。
相关入口文件:宿主模板 README、宿主 MainActivity、宿主依赖声明、宿主仓库配置、devicelab 测试任务。
【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考