26 · 混合开发与 Add-to-App
目标:把 Flutter 模块嵌入已有的原生 App,或直接接入原生能力——这是老项目渐进式迁移的真实场景(HaierEnergy 本身就是从 uni-app 迁移而来)。
开篇:前端概念对照
| 前端 / Hybrid | Flutter 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 环境) |
# 在原生工程同级目录创建 module
flutter create -t module my_flutter_module
# 产出产物给原生集成(团队隔离方案)
flutter build aar # Android → AAR
flutter build ios-framework --release # iOS → Framework三、Android 接入
3.1 引擎预热(性能的关键)
不要每次打开页面才创建引擎——首次创建 FlutterEngine 有几百毫秒开销。
// 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 页面
// 复用缓存引擎(启动快,推荐)
startActivity(
FlutterActivity
.withCachedEngine("main_engine")
.build(this)
)
// 或新建引擎(需要传自定义入口/路由时用)
startActivity(
FlutterActivity
.withNewEngine()
.initialRoute("/order/123") // 传初始路由
.build(this)
)⚠️ 用 withNewEngine() 每次都有初始化开销;高频页面必须用缓存引擎。
3.3 嵌入 Fragment(部分是原生、部分是 Flutter 时)
val fragment = FlutterFragment.withCachedEngine("main_engine").build()
supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, fragment)
.commit()四、iOS 接入
// 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
}// 打开 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 消费,空了再交给原生 |
// 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:下发初始参数/路由
// 原生在引擎启动后通过 Channel 下发(比 initialRoute 更可靠)
MethodChannel(engine.dartExecutor.binaryMessenger, "com.haier.energy/native")
.invokeMethod("navigateTo", mapOf("route" to "/order/123", "id" to "123"))// Flutter 侧监听
_channel.setMethodCallHandler((call) async {
if (call.method == 'navigateTo') {
final args = Map<String, dynamic>.from(call.arguments);
router.go(args['route']);
}
});6.2 Flutter → 原生:请求关闭页面 / 调用原生功能
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)
// 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 方式)
# 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/控制台 |
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 与桌面端 | 返回 专题总览