Skip to content

26 · 混合开发与 Add-to-App ​

目标:把 Flutter 模块嵌入已有的原生 App,或直接接入原生能力——这是老项目渐进式迁移的真实场景(HaierEnergy 本身就是从 uni-app 迁移而来)。

衔接:平台通道通信见 21 篇,路由见 13 篇。


开篇:前端概念对照 ​

前端 / HybridFlutter Add-to-App
WebView 套壳(Cordova/Capacitor)不是 WebView:Flutter 是原生渲染引擎,性能与原生一致
JSBridge 通信Platform Channel(见 21 篇)
SPA 路由Flutter 内部 GoRouter + 原生栈协调
微前端(qiankun / single-spa)多 FlutterEngine 实例(思想类似"多应用实例",但重得多)
前端独立发布❌ Flutter 模块需跟随原生 App 发版(无官方热更新)

核心认知:Add-to-App 是"原生 App 里嵌一块 Flutter",宿主是原生工程。所以构建、发版、生命周期都由原生主导——这与纯 Flutter 项目的心智完全不同。


一、三种集成形态与选型 ​

形态说明适用复杂度
全 Flutter整个 App 都是 Flutter,仅少量原生代码新项目低
Add-to-App(模块嵌入)原生 App 为主,Flutter 承接若干页面/功能老项目渐进迁移(你的场景)中高
多引擎混合(FlutterEngineGroup)多个 Flutter 实例并存需要独立生命周期的多个模块高

决策建议:

新项目 / 可以重写        → 全 Flutter
老项目、需要渐进迁移     → Add-to-App
只在少数页面用 Flutter   → Add-to-App(先挑 1~2 个页面试点)
已有大量原生,只想加个功能 → 先评估:用原生实现是否更快

⚠️ 不要为了用 Flutter 而用 Flutter:单个页面改造的成本包含引擎接入、路由打通、数据互通、构建集成,第一个页面最贵,后面才便宜。


二、接入方式:三种,推荐第二种 ​

方式说明适用
flutter create -t module产出可嵌入的 Flutter Module推荐(绝大多数场景)
源码依赖(path:)原生工程直接引 Flutter 源码需要频繁改 Flutter 代码时
AAR / Framework 产物把 Flutter 打成二进制给原生集成团队隔离(原生同学不装 Flutter 环境)
bash
# 在原生工程同级目录创建 module
flutter create -t module my_flutter_module

# 产出产物给原生集成(团队隔离方案)
flutter build aar          # Android → AAR
flutter build ios-framework --release   # iOS → Framework

三、Android 接入 ​

3.1 引擎预热(性能的关键) ​

不要每次打开页面才创建引擎——首次创建 FlutterEngine 有几百毫秒开销。

kotlin
// MyApplication.kt —— 应用启动时预热
class MyApplication : Application() {
    lateinit var flutterEngine: FlutterEngine
        private set

    override fun onCreate() {
        super.onCreate()
        flutterEngine = FlutterEngine(this).apply {
            dartExecutor.executeDartEntrypoint(
                DartExecutor.DartEntrypoint.createDefault()
            )
        }
        // 预热缓存,供 FlutterActivity 复用
        FlutterEngineCache.getInstance().put("main_engine", flutterEngine)
    }
}

3.2 打开 Flutter 页面 ​

kotlin
// 复用缓存引擎(启动快,推荐)
startActivity(
    FlutterActivity
        .withCachedEngine("main_engine")
        .build(this)
)

// 或新建引擎(需要传自定义入口/路由时用)
startActivity(
    FlutterActivity
        .withNewEngine()
        .initialRoute("/order/123")          // 传初始路由
        .build(this)
)

⚠️ 用 withNewEngine() 每次都有初始化开销;高频页面必须用缓存引擎。

3.3 嵌入 Fragment(部分是原生、部分是 Flutter 时) ​

kotlin
val fragment = FlutterFragment.withCachedEngine("main_engine").build()
supportFragmentManager.beginTransaction()
    .add(R.id.fragment_container, fragment)
    .commit()

四、iOS 接入 ​

swift
// AppDelegate.swift —— 预热引擎
lazy var flutterEngine = FlutterEngine(name: "main_engine")

override func application(_ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    flutterEngine.run()                                    // 预热
    GeneratedPluginRegistrant.register(with: flutterEngine)
    return true
}
swift
// 打开 Flutter 页面
let flutterVC = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
flutterVC.setInitialRoute("/order/123")      // 注意需在 engine.run() 前设置才百分百生效
self.navigationController?.pushViewController(flutterVC, animated: true)

iOS 特有坑:setInitialRoute 在引擎已 run() 之后调用可能不生效。稳妥做法是通过 MethodChannel 在 Flutter 启动后下发路由(第六节)。


五、路由打通:最容易做错的部分 ​

混合开发的路由有两套栈:原生栈和 Flutter 内部栈。

原生栈:  [原生首页] → [Flutter 容器页] → [原生设置页]
                          ↓
Flutter 内部栈:      [列表页] → [详情页]

必须提前定清楚三件事:

问题建议
哪些页面在原生、哪些在 Flutter画清楚边界,避免"半原生半 Flutter"的模糊页面
Flutter 内部还能不能跳回原生能,通过 Channel 通知原生(第六节)
物理返回键 / 手势返回谁处理Flutter 内部栈非空时由 Flutter 消费,空了再交给原生
dart
// Flutter 侧:拦截返回,栈空了再交还原生
PopScope(
  canPop: await router.canPop(),        // GoRouter 判断内部栈是否可返回
  onPopInvokedWithResult: (didPop, _) {
    if (!didPop) {
      const MethodChannel('com.haier.energy/native')
          .invokeMethod('finishFlutterPage');   // 通知原生关闭容器
    }
  },
  child: child,
)

六、数据与方法互通 ​

通信机制就是 Platform Channel(21 篇),这里补充混合开发特有的三个模式:

6.1 原生 → Flutter:下发初始参数/路由 ​

kotlin
// 原生在引擎启动后通过 Channel 下发(比 initialRoute 更可靠)
MethodChannel(engine.dartExecutor.binaryMessenger, "com.haier.energy/native")
    .invokeMethod("navigateTo", mapOf("route" to "/order/123", "id" to "123"))
dart
// Flutter 侧监听
_channel.setMethodCallHandler((call) async {
  if (call.method == 'navigateTo') {
    final args = Map<String, dynamic>.from(call.arguments);
    router.go(args['route']);
  }
});

6.2 Flutter → 原生:请求关闭页面 / 调用原生功能 ​

dart
await const MethodChannel('com.haier.energy/native')
    .invokeMethod('openNativePage', {'page': 'settings'});

6.3 共享数据(登录态、Token) ​

不要各存一份。推荐:

方案说明
原生侧为数据源Flutter 通过 Channel 向原生要 Token(单一数据源,最安全)
共享存储用同一个 shared_preferences / Keychain 分组(需原生配置)

登录态共享出错会导致"原生已登录但 Flutter 显示未登录",这类 bug 很常见且难查。


七、构建集成 ​

7.1 Android(settings.gradle 引入 module) ​

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

// app/build.gradle
dependencies {
    implementation project(':flutter')
}

7.2 iOS(推荐用 CocoaPods 或 framework 方式) ​

ruby
# Podfile —— 开发期用源码路径,发布期改用 framework 产物
flutter_application_path = '../my_flutter_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'MyApp' do
  install_all_flutter_pods(flutter_application_path)
end

团队隔离:原生同学不装 Flutter 环境时,把 Flutter 打成 AAR/Framework 提交到制品库,原生工程只依赖二进制产物。


八、调试 ​

需求做法
热重载在 module 目录执行 flutter attach,再操作原生 App 进入 Flutter 页面
断点调试 Flutter 代码VSCode/Android Studio 里 flutter attach 后正常打断点
调试原生与 Flutter 交界原生用 Android Studio/Xcode,Flutter 用 Flutter 工具,两个 IDE 同时开
看 Flutter 日志flutter attach 后控制台;或原生 logcat/控制台
bash
cd my_flutter_module
flutter attach          # 然后操作 App 进入 Flutter 页面即可连上

九、常见陷阱速查 ​

陷阱症状解法
未预热引擎首次打开 Flutter 页面卡顿 300ms+Application 里预热 + FlutterEngineCache
每次 withNewEngine()每次打开都慢、内存涨改用 withCachedEngine
多引擎滥用内存暴涨(每个引擎约 20~30MB)尽量单引擎;确需多实例用 FlutterEngineGroup
返回键行为错乱Flutter 内返回直接退出整个页面PopScope 判断内部栈,空了再通知原生
登录态不同步原生已登录、Flutter 未登录统一数据源:Flutter 向原生取
setInitialRoute 不生效(iOS)总是跳首页改为 Channel 在启动后下发路由
构建产物不一致原生打出的包 Flutter 代码是旧的先 flutter build aar/ios-framework,再打原生包
插件未注册用到某插件时报错确认 GeneratedPluginRegistrant.register 已调用(尤其自建引擎时)
热重载失效改 Flutter 代码没反应用 flutter attach(不是 flutter run)
包体增大接入后 APK 增大约 5~15MB用 ABI 分包(--split-per-abi,见 23 篇)

十、动手练习 ​

练习 26.1(最小集成):新建一个 Flutter Module,在 Android 原生工程里用 FlutterActivity.withCachedEngine 打开它,对比"预热"与"不预热"的首次打开耗时。

练习 26.2(路由互通):实现原生传参打开 Flutter 的指定页面(用 Channel 下发路由而非 initialRoute),并在 Flutter 内部栈返回到底时通知原生关闭容器。

练习 26.3(返回键):在 Android 上验证——Flutter 内部有多层页面时返回应逐层退出,到最底层时关闭整个 Flutter 容器回到原生。

练习 26.4(共享登录态):用 Channel 让 Flutter 向原生请求 Token,验证"原生登出后 Flutter 也同步显示未登录"。

练习 26.5(进阶):把 Flutter module 打成 AAR,让原生工程不依赖 Flutter 环境也能构建——这是团队协作的关键一步。


十一、自检清单 ​

  • [ ] 能说出三种集成形态的适用场景与选型依据
  • [ ] 知道引擎预热的重要性与 FlutterEngineCache 的用法
  • [ ] 能处理混合栈的返回键/手势返回(Flutter 内部栈优先)
  • [ ] 知道登录态应该以原生为单一数据源
  • [ ] 能说出 AAR/Framework 产物方式对团队协作的意义
  • [ ] 知道 flutter attach 是混合开发的调试入口(不是 flutter run)

导航:上一章 25 · Flutter Web 与桌面端 | 返回 专题总览