首页
/ Flutter 官方 add-to-app 集成测试:带自定义 Build Type 与 Flavor 的 Android 宿主应用(v2 嵌入方式)解析

Flutter 官方 add-to-app 集成测试:带自定义 Build Type 与 Flavor 的 Android 宿主应用(v2 嵌入方式)解析

2026-09-06 11:33:55作者:邵娇湘

本文基于 Flutter 仓库中 module_host_with_custom_build_v2_embedding 目录的 README 文档及其配套源码展开,介绍这个用于验证「Flutter 模块嵌入原生 Android 宿主应用」场景的完整工程:它如何以 v2 嵌入方式接入一个 flutter create -t module 生成的模块,如何通过 matchingFallbacksproductFlavors 等 Gradle 配置支持宿主应用自定义的 build type 与 flavor,以及 devicelab 自动化测试如何驱动多轮 APK 构建来验证产物完整性。读完后,你能够理解 Flutter 模块嵌入 Android 应用(add-to-app)的宿主侧工程结构,并掌握自定义构建变体场景下的关键配置与验证手段。

一、这个目录是什么:一个「模块宿主」Android 应用

README.md 对目录定位的说明非常明确:

Android host app for a Flutter module created using flutter create -t module hello and placed in a sibling folder to (a clone of) the host app. Used by the module_host_with_custom_build_test.dart device lab test.

可以归纳为三个要点:

  1. 角色:它本身不是一个可直接 gradlew assemble 就跑起来的独立应用,而是一个宿主应用(host app)模板,专门用来承载一个 Flutter module;
  2. 模块来源:与之配套的 Flutter module 由 flutter create -t module hello 创建,并被放置在宿主应用克隆目录的**同级目录(sibling folder)**下,目录名约定为 hello——这一点与 settings.gradle 中的相对路径引用严格对应(见下一节);
  3. 消费者:它被 devicelab 设备实验室任务 module_host_with_custom_build_test.dart 使用,用于验证「含有 Flutter 模块的 Android 应用在拥有自定义 build type 与 flavor 时能否正常构建」。

目录名中的 v2_embedding 点明了嵌入方式:使用 Flutter Android Embedding v2,即通过 io.flutter.embedding.android.FlutterActivity 这类 v2 API 承载 Flutter UI,而非已废弃的 v1 io.flutter.app.FlutterActivity。从源码结构看,整个宿主侧 Java 代码只有一个入口类,这也是 v2 嵌入的典型形态。

二、核心文件走读:模块是如何「挂」进宿主工程的

该目录的完整文件清单如下(均为仓库实际存在文件):

2.1 settings.gradle:一行代码完成模块集成

settings.gradle 的内容:

include ':app'
setBinding(new Binding([gradle: this]))
evaluate(new File(settingsDir.parentFile, 'hello/.android/include_flutter.groovy'))

三行分别承担:

  • include ':app':注册宿主自身的 :app 模块;
  • setBinding(new Binding([gradle: this])):把当前 Settings 实例注入 binding,供后续脚本以 gradle 变量访问;
  • evaluate(new File(settingsDir.parentFile, 'hello/.android/include_flutter.groovy')):这是 add-to-app 的关键一步。settingsDir.parentFile 指向宿主工程目录的上一级,再加上 hello/.android/include_flutter.groovy,恰好对应 README 中「module 与宿主是 sibling 目录」的约定。

include_flutter.groovyflutter create -t module 时由 Flutter 工具链生成、并随 flutter pub get 刷新进模块 .android 目录的脚本,它会向宿主工程注册 :flutter 子工程及 Flutter 构建所需的插件与依赖(该脚本本身不在本仓库中,由模块工程在运行时生成)。因此,README 描述的「先 flutter create -t module hello,再把模块放到宿主同级目录」正是 settings.gradle 能够解析成立的前提

2.2 gradle.properties:AndroidX 与 JVM 内存

gradle.properties 只有两行:

org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
  • android.useAndroidX=true 声明宿主使用 AndroidX 体系,这是 v2 嵌入(androidx.* 依赖)的硬性要求;
  • 8G 堆内存的 JVM 参数则反映了 CI 构建(同时构建 debug/release、多个 ABI、多变体)对 Gradle daemon 内存的现实需求,并开启了 OOM 时堆转储以便排查。

2.3 app/build.gradle:自定义 build type 与 flavor 的主战场

app/build.gradle 是理解「custom build」这一主题的核心,逐项拆解:

android {
    namespace = "io.flutter.addtoapp"
    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.addtoapp"
        minSdk = 24
        targetSdk = 36
        versionCode = 1
        versionName = "1.0"
    }
    // Test build types.
    buildTypes {
        staging {
            initWith debug
            // This is required because the `:flutter` project doesn't define this custom build type.
            // Without the fallback, the Android plugin will make Gradle exit with the following error:
            // `Unable to find a matching variant of project :flutter`
            matchingFallbacks += 'debug'
        }
        prod {
            initWith release
            matchingFallbacks += 'release'
        }
    }
    // Test flavors.
    flavorDimensions += "version"
    productFlavors {
        demo {
            dimension "version"
        }
    }
}

dependencies {
    implementation project(':flutter')
}

关键配置与原理:

  • staging / prod 两个自定义 build typebuild.gradle L24-L37):staginginitWith debug 派生自 debug,prodinitWith release 派生自 release。这模拟了真实业务中常见的「在 debug/release 之外再定义预发(staging)、正式(prod)通道」的宿主工程;
  • matchingFallbacks 是 add-to-app 场景最容易踩的坑:Flutter 模块生成的 :flutter 子工程只有标准的 debug/release 变体,当宿主定义了 staging 这类自定义 build type 时,Gradle 变体匹配机制会报 Unable to find a matching variant of project :flutter。源码注释里明确写明了这一点,解法就是 matchingFallbacks += 'debug'(或 'release'),告诉 Gradle「找不到同名变体时回落到标准变体」。这是本文档配套工程对 add-to-app 用户最具迁移价值的经验;
  • flavorDimensions += "version" + productFlavors { demo { ... } }L38-L44):定义了名为 demo 的产品风味。注意 demo宿主专属的 flavor,:flutter 工程同样没有它——这正是整套测试要覆盖的核心矛盾:当 flavor 与 build type 同时自定义时,APK 里的 Flutter 资产是否仍然完整。与 build type 不同,productFlavors 不需要为 :flutter 配置 matchingFallbacks,因为 flavor 匹配默认按「存在性」处理,但 build type 必须显式声明 fallback;
  • implementation project(':flutter'):这是 v2 嵌入下宿主模块依赖 Flutter 模块的标准写法,:flutter 工程即由 2.1 节中 include_flutter.groovy 注册而来;
  • ndkVersion = "28.2.13676358":源码注释特别说明该版本必须与 CI 配方从 CIPD 拉取的 NDK 版本完全一致,这是 CI 环境中 AOT 编译(release 模式的 libapp.so)能命中缓存、避免重复下载/编译的前提;
  • minSdk = 24compileSdk = 36、Java 17 源码/目标兼容级别:界定了该集成场景验证的最低 API 与工具链基线。

2.4 入口 Activity 与 Manifest:极简的 v2 嵌入宿主

MainActivity.java 全文即一个空壳:

package io.flutter.addtoapp;

import io.flutter.embedding.android.FlutterActivity;

public class MainActivity extends FlutterActivity {
}

io.flutter.embedding.android.FlutterActivity 来自 :flutter 工程提供的 v2 嵌入库,它会在启动时加载 Flutter 引擎、执行模块中的 Dart 入口(即 hello module 的 lib/main.dart)。这个「什么都不写」的入口本身就是验证点:只要继承 FlutterActivity 就能跑通,说明 Flutter 模块的引擎、Dart 产物与宿主集成链路健康

AndroidManifest.xml 同样精简:allowBackup="false",并用 tools:ignore 忽略缺失图标与 GoogleAppIndexing 警告,仅声明 .MainActivity 一个 Activity。测试宿主不需要图标、启动器等 UI 元素,聚焦构建正确性本身。

2.5 Gradle Wrapper:固定构建工具链

gradle-wrapper.properties 锁定 gradle-8.14-bin.zip 发行版(validateDistributionUrl=true),保证 devicelab 任务在任何 CI 机器上使用的 Gradle 版本可复现。一个值得注意的细节:devicelab 任务在拷贝宿主模板时,还会把模块工程 .android 目录下的 gradlew 脚本和 gradle-wrapper.jar 覆盖拷入宿主工程(见 4 节),因为 Flutter 模块的 .android 侧维护着与工具链匹配的 wrapper 实现,宿主模板里只保留 .properties 配置文件。

三、devicelab 验证流程:README 中那个测试任务到底做了什么

README 指明本目录「Used by the module_host_with_custom_build_test.dart device lab test」,对应实现为 module_host_with_custom_build_test.dart。其头部注释概括了测试目标:

Tests that the Android app containing a Flutter module can be built when it has custom build types and flavors.

整个任务的执行流程(L18-L274)可以整理为:

  1. 定位 JavafindJavaHome(),找不到直接判失败;
  2. 预取与创建模块:执行 flutter precache --android --no-ios,然后在系统临时目录中 flutter create --org io.flutter.devicelab --template=module hello,再对模块执行 flutter pub get
  3. 布置宿主工程:把仓库中的 dev/integration_tests/module_host_with_custom_build_v2_embedding 整目录递归拷贝到临时目录 hello_host_app_with_custom_build——与模块 hello 恰好构成 sibling 关系,满足 settings.gradle 的路径约定;随后拷贝模块 .android 下的 gradlewgradle-wrapper.jar,非 Windows 平台还要 chmod +x gradlew
  4. 四轮构建 + 产物校验(每轮之间先 gradlew clean):
    • app:assembleDemoDebug:产出 app/build/outputs/apk/demo/debug/app-demo-debug.apk,解包校验其中包含预期的 Flutter 资产快照(flutterAssets / debugAssets 清单);
    • 任务顺序扰动测试L124-L164):默认情况下 processDemoDebugManifest 先于 mergeDemoDebugAssets 执行,这里故意反转顺序,在同一命令行里先跑 app:mergeDemoDebugAssets、再 app:processDemoDebugManifest、最后 app:assembleDemoDebug,再次校验 APK 内 Flutter 资产完整。源码注释指明该场景对应一个历史上的上游 PR 修复(注释中引用了 PR 编号 41333),本质是回归保护:无论 Gradle 任务执行顺序如何,Flutter 资产都不能丢失;
    • app:assembleDemoStaging:验证 staginginitWith debug + matchingFallbacks 'debug')变体产出 app-demo-staging.apk 且资产完整;
    • app:assembleDemoReleaseapp:assembleDemoProd:验证 release 系变体,校验标准从「资产快照」升级为按 ABI 校验 AOT 产物——lib/arm64-v8a/lib/armeabi-v7a/ 下的 libflutter.so(引擎)和 libapp.so(Dart AOT 编译产物)均须存在于 APK 中。

这套「多变体 × 双模式(解释执行资产 / AOT 库)× 任务顺序扰动」的矩阵,正是「custom build」测试名称的完整含义。

四、手动复现路径与适用前提

如果你想在自己的环境里按本目录的设计手动演练(注意:以下仅为查看与运行说明,不需要也不应该修改本仓库任何文件),流程与 devicelab 任务一致:

  1. 准备 JDK(任务里通过 JAVA_HOME 显式指定),并执行 flutter precache --android
  2. 在任意工作目录下执行 flutter create --template=module hello,进入 hello 执行 flutter pub get
  3. module_host_with_custom_build_v2_embedding 目录整体复制到 hello同级位置并改名为宿主工程目录(例如 hello_host_app),然后把 hello/.android/gradlewhello/.android/gradle/wrapper/gradle-wrapper.jar 拷入宿主工程对应位置;
  4. 在宿主工程目录依次执行:
    gradlew clean
    gradlew app:assembleDemoDebug
    gradlew app:assembleDemoStaging
    gradlew app:assembleDemoRelease
    gradlew app:assembleDemoProd
    
  5. 检查 app/build/outputs/apk/demo/{debug,staging,release,prod}/ 下的 APK:debug/staging 变体应含 flutter_assets 相关资产;release/prod 变体应含对应 ABI 的 libflutter.solibapp.so

适用前提需要说明:settings.gradlehello 目录名的硬编码约定、app/build.gradlendkVersion 必须与构建机 CIPD 下发的 NDK 版本一致(本地构建如 NDK 版本不符,Gradle 会尝试自行下载对应 NDK),以及 release 变体构建需要 AOT 编译链路的完整工具链。

五、这篇「小 README」背后的工程价值

回头看,README.md 只有短短几行,但它锚定的是一整套可运行的验证资产:一个最小化 v2 嵌入宿主、一份针对「宿主自定义 build type / flavor 与 :flutter 标准变体不匹配」这一真实高频场景的 Gradle 配置范式(matchingFallbacks 回退),以及一个覆盖 debug/staging/release/prod 四变体、含任务顺序回归保护的 devicelab 任务。对于把 Flutter module 嵌入已有 Android 应用、且宿主工程带有企业级多构建通道的团队,该目录提供了一个可直接对照的参考实现——尤其是 app/build.gradlestaging/prod 两个 build type 与 demo flavor 的组合,基本覆盖了 add-to-app 文档体系中「自定义构建变体」这一节需要验证的全部边界条件。

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