news 2026/9/7 3:33:07

Flutter add-to-app 实战:用纯 Android 宿主应用验证 Flutter Module 的 V2 Embedding 集成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flutter add-to-app 实战:用纯 Android 宿主应用验证 Flutter Module 的 V2 Embedding 集成

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_debugflutter_profileflutter_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 = 24targetSdk = 36compileSdk = 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.48.2.1AGP 8.3 之前的旧链路
8.13-rc-18.8.1AGP 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'):

  1. 查找 JavafindJavaHome(),后续所有 Gradle 命令注入JAVA_HOME
  2. 创建 Flutter Moduleflutter create --org io.flutter.devicelab --template=module hello,生成与 README 描述一致的 Module 工程。
  3. 注入 FFI 包与只读资源:创建一个名为ffi_package的本地 FFI 包加入 Module 依赖,并在assets/下生成read-only.txt(非 Windows 上chmod 444)。这两个注入项用于后续验证原生.so与资源文件访问权限是否正确进入宿主 APK。
  4. 构建 Flutter Module 库归档:在 Module 的.android目录执行gradlew flutter:assembleDebug,断言产出.android/Flutter/build/outputs/aar/flutter-debug.aar(第 124-150 行)。
  5. 构建 ephemeral 宿主 APKflutter build apk,检查build/host/outputs/apk/release/app-release.apk存在;接着flutter clean再构建一次,验证 clean 后可重建(editable 模式)。
  6. 验证flutter build aarflutter build aar --no-profile直接构建 AAR 成功。
  7. 接入本文的宿主模板:把dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding递归拷贝到临时目录的hello_host_app,并从 Module 的.android目录拷入gradlewgradle-wrapper.jar,然后替换REPLACEME为指定 Gradle / AGP 版本(第 217-253 行)。
  8. 构建宿主 debug APK 并校验内容gradlew app:assembleDebug后,用 apk_utils 解包 APK 断言包含flutter_assets关键文件、lib/arm64-v8a/libflutter.solibapp.so以及lib/arm64-v8a|armeabi-v7a/libffi_package.so(FFI 包的两个 ABI 产物),并校验 manifest 中的flutterProjectType = module元数据。
  9. 只读资源权限检查:定位中间产物app/build/intermediates/assets/debug[/mergeDebugAssets]/flutter_assets/assets/read-only.txt,要求文件权限以rw开头——即源文件是 444 只读,进入 APK 后必须对用户可读(否则运行时RootBundle加载会失败),这是对 Gradle merge assets 行为的一次真实回归。
  10. 构建宿主 release APK 并校验gradlew app:assembleRelease后检查app-release-unsigned.apk,断言两个 ABI 的libflutter.so/libapp.so/libffi_package.so齐全,并解包assets/flutter_assets/NOTICES.Z(gzip + utf8 解码后须包含skiaFlutter Authors字样)验证开源许可证文件正确生成。
  11. 构建过程日志断言:检查 stderr 中不得出现You are applying Flutter's main Gradle plugin imperatively——即要求宿主工程以声明式(plugins {}/ 依赖方式)而非命令式apply方式接入 Flutter Gradle 插件,防止构建链路退化到旧写法。

任一步失败,任务即返回TaskResult.failurefinally块清理临时目录。测试由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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 3:31:47

视频真实性验证技术:从元数据分析到深度学习检测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 3:30:02

AI代理权限管理实战:从越界检测到分层联防

122次测试&#xff0c;10次越界。这是我最近在搭建AI代理自动化测试框架时&#xff0c;压测跑出来的真实数据。说句实话&#xff0c;第一次看到这个数字的时候&#xff0c;我的第一反应是测试环境被人动了手脚&#xff0c;查了半天才发现&#xff0c;问题压根不在测试环境上&am…

作者头像 李华
网站建设 2026/9/7 3:26:24

KVM切换器从原理到实战:多主机共享键鼠与显示器

桌面上一共两台主机&#xff1a;一台 Windows 处理日常办公和沟通&#xff0c;一台 Linux 用来写代码、跑实验。显示器、键盘、鼠标只有一套&#xff0c;平时切换靠插拔。每天早上到工位&#xff0c;先低头找线&#xff0c;把鼠标接收器从 A 机拔下来插到 B 机&#xff0c;再把…

作者头像 李华
网站建设 2026/9/7 3:25:20

用本地AI保护简历隐私:从JD解析到求职信生成的完整实践

你上一次把简历粘贴进在线聊天框让AI帮你改&#xff0c;是什么时候&#xff1f;我这么问不是在质疑AI写求职信的能力——我自己也这么干过很多次。但有一次&#xff0c;我把一份完整简历丢给一个云端写作工具之后&#xff0c;脑子里突然冒出一个问题&#xff1a;这份包含我电话…

作者头像 李华