拒绝重写!Flutter Add-to-App 全攻略:让原生应用“渐进式”拥抱跨平台

0 阅读8分钟

为什么我们需要 Add-to-App?

unwatermarked_Gemini_Generated_Image_51ku3f51ku3f51ku.png 在移动开发领域,Flutter 的跨平台优势(Write once, run anywhere)毋庸置疑。但在现实世界中,我们往往面临着沉重的“历史包袱”。

痛点场景:

“我们公司有一个维护了 5 年的电商 App,原生代码几十万行。最近老板嫌 UI 迭代慢,想用 Flutter,但完全重写是不可能的——业务线太长,风险太大。我们要的是渐进式的改变。”

这就是 Add-to-App 存在的意义。它允许我们将 Flutter 视为一个“库”或“模块”,嵌入到现有的 Android 或 iOS 应用中。

它的核心价值在于:

  1. 成本控制:无需抛弃现有的原生资产(支付模块、复杂的底层算法等)。
  2. 渐进迁移:可以从一个非核心页面(如“关于我们”或“活动页”)开始,逐步扩大 Flutter 的版图。
  3. 复用能力:新开发的 Flutter 模块可以直接在 Android 和 iOS 甚至 Web 上复用,从一开始就享受跨平台红利。

Add-to-App 的基本概念与原理

什么是 Add-to-App?

简单来说,Add-to-App 就是把 Flutter 环境(Dart VM + Flutter Engine)打包成一个原生组件(View 或 ViewController/Activity),塞进现有的原生 App 里。

  • 对于 Android:Flutter 只是一个 View,或者一个 Activity/Fragment。
  • 对于 iOS:Flutter 只是一个 UIView,或者 FlutterViewController。

运行模式:多引擎 vs 多视图

在混合开发中,理解 Flutter 的“寄生”方式至关重要:

策略描述优点缺点
单引擎复用 (Single Engine)全局维护一个 Engine,在不同原生页面间跳转时,通过 attach/detach 挂载到当前界面。内存占用最低;状态不仅共享且保持。导航栈管理极其复杂(原生页面 A -> Flutter B -> 原生 C -> 返回 B 时需恢复现场)。
多引擎 (Multi-Engine)每次打开 Flutter 页面都创建一个新 Engine。逻辑隔离,互不干扰;导航栈管理简单。内存爆炸(每个 Engine 默认消耗较大),启动延迟明显。
FlutterEngineGroup (推荐)官方提供的轻量级多引擎方案(Flutter 2.0+)。多个 Engine 共享 GPU 上下文、字体和代码段,新增一个 Engine 仅需 ~180KB 内存Dart Isolate 彼此隔离,状态不共享(需通过数据层同步)。

误区提示:桌面端/Web 支持的“多视图(Multi-view)”模式(即一个 Engine 渲染多个窗口)目前尚未在移动端 Add-to-App 场景中稳定支持。在移动端,请优先考虑 FlutterEngineGroup

最佳实践场景

  • 高频迭代的业务模块:如电商的活动页、个人中心。
  • 复杂的 UI 交互:如需要高性能动画的图表页。
  • 统一逻辑:双端逻辑完全一致的表单提交或业务计算。

实战 I:在 Android 原生 App 中嵌入 Flutter

创建 Flutter Module

注意,我们不能 flutter create my_app,因为我们不需要一个完整的 App 壳子,我们需要的是一个模块

# 在原生项目同级目录下执行
flutter create -t module my_flutter_module

执行后,你会发现生成的目录结构中,androidios 文件夹是隐藏的(.android, .ios),因为它们是自动生成的包装器。

将 Flutter Module 导入 Android 项目

自 Flutter 3.x 起,官方推荐通过 Gradle 脚本自动管理依赖,避免手动编写 implementation 导致的版本冲突。

步骤 1:修改 settings.gradle

在 include ':app' 之后加入:

// 绑定 Flutter 模块构建脚本
setBinding(new Binding([gradle: this]))
evaluate(new File(
  settingsDir.parentFile, // 假设 flutter_module 与当前项目同级
  'my_flutter_module/.android/include_flutter.groovy'
))

步骤 2:修改 app/build.gradle

依赖会自动注入,通常无需手动添加 implementation project(':flutter')。但需确保 compileSdkVersion 与 Flutter 模块要求一致(通常需 API 33+)。

在 Android 上渲染 Flutter (Activity 与 Fragment)

方式 A:使用 FlutterActivity(全屏场景)

适合独立的业务流程,如“个人中心”或“设置页”。

// 使用缓存 Engine 启动(推荐)
startActivity(
    FlutterActivity
        .withCachedEngine("my_engine_id")
        .build(this)
);

方式 B:使用 FlutterFragment(局部嵌入)

适合将 Flutter 作为一个 View 块嵌入原生页面,例如在一个原生 Tab 页中展示 Flutter 列表。

// 在原生 Activity 或 Fragment 中
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager
    .beginTransaction()
    .replace(R.id.fragment_container, 
             FlutterFragment.withCachedEngine("my_engine_id").build())
    .commit();

性能优化 Tip:使用缓存 Engine

withNewEngine() 会导致每次打开页面都有明显的“白屏”或加载延迟。推荐使用 FlutterEngineCache 进行预热:

// 1. 在 Application 启动时预热
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // 实例化 Engine
        FlutterEngine flutterEngine = new FlutterEngine(this);
        // 开始执行 Dart 代码(预加载)
        flutterEngine.getDartExecutor().executeDartEntrypoint(
            DartExecutor.DartEntrypoint.createDefault()
        );
        // 存入缓存
        FlutterEngineCache
            .getInstance()
            .put("my_engine_id", flutterEngine);
    }
}

// 2. 启动时使用缓存的 Engine
startActivity(
    FlutterActivity
        .withCachedEngine("my_engine_id")
        .build(this)
);

实战 II:在 iOS 原生 App 集成 Flutter

创建 Flutter Module

(同上,使用同一个 my_flutter_module 即可)

CocoaPods 集成

这是 iOS 最标准的集成方式。

修改 Podfile

在 iOS 工程的 Podfile 中添加脚本钩子:

# Podfile
platform :ios, '14.0'

# 定义 Flutter 模块路径
flutter_application_path = '../my_flutter_module'

# 加载 Flutter 的 Pod 助手脚本
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'MyApp' do
  use_frameworks!
  
  # 安装 Flutter 依赖
  install_all_flutter_pods(flutter_application_path)
end

执行 pod install,你会发现 Flutter 相关的 Framework 已经被链接进来了。

在 iOS 中打开 Flutter View

使用 FlutterViewController

import Flutter

// 在某个按钮点击事件中
@objc func showFlutter() {
    // 获取 Flutter Engine(同样建议使用 Cache,这里演示简单模式)
    let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
    
    let flutterViewController = FlutterViewController(
        engine: flutterEngine, 
        nibName: nil, 
        bundle: nil
    )
    
    present(flutterViewController, animated: true, completion: nil)
}

在 iOS 中使用缓存 Engine

为了避免点击按钮时卡顿,强烈建议在 App 启动时预热 Engine。

步骤 1:在 AppDelegate 中初始化并缓存

import UIKit
import Flutter
import FlutterPluginRegistrant // 用于注册插件

@main
class AppDelegate: FlutterAppDelegate { // 继承 FlutterAppDelegate
  
  lazy var flutterEngine = FlutterEngine(name: "my_engine_id")

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    // 1. 运行 Engine (预热)
    flutterEngine.run();
    // 2. 注册插件(关键!否则 Flutter 里的插件无法使用)
    GeneratedPluginRegistrant.register(with: flutterEngine);
    
    return super.application(application, didFinishLaunchingWithOptions: launchOptions);
  }
}

步骤 2:使用缓存 Engine 弹出页面

@objc func showFlutter() {
    let appDelegate = UIApplication.shared.delegate as! AppDelegate
    let flutterEngine = appDelegate.flutterEngine
    
    let flutterVC = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
    present(flutterVC, animated: true, completion: nil)
}

进阶:原生与 Flutter 的双向通信 (MethodChannel)

当混合开发时,不可避免地需要数据交互:Flutter 读取原生的 Token,或者原生调用 Flutter 的刷新方法。MethodChannel 是最常用的桥梁。

5.1 Flutter 端 (Dart)

import 'package:flutter/services.dart';

class NativeBridge {
  static const platform = MethodChannel('com.example.app/data');

  // 调用原生方法
  Future<String> getUserToken() async {
    try {
      final String token = await platform.invokeMethod('getToken');
      return token;
    } on PlatformException catch (e) {
      return "Failed: '${e.message}'.";
    }
  }
}

5.2 Android 端

// 需在 Engine 初始化后注册 Channel
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "com.example.app/data")
    .setMethodCallHandler(
        (call, result) -> {
            if (call.method.equals("getToken")) {
                // 执行原生逻辑获取 Token
                String token = MyAuthManager.getToken();
                result.success(token);
            } else {
                result.notImplemented();
            }
        }
    );

注意事项:

MethodChannel 并非能传递任意对象,它的底层依赖 BinaryMessenger 进行二进制流传输。

  • StandardMethodCodec(标准编解码器) : Flutter 默认使用此 Codec,它只支持高效序列化以下基础类型

    • null, bool, int, double, String
    • List, Map (仅限上述基础类型的集合)
    • 二进制数据 (Uint8List / byte[])

注意:如果你尝试直接传递一个自定义类 User,通道会报错。 解决方案:将对象转为 JSON String 或 Map 进行传递,或者自定义 Codec。


进阶:混合栈管理与多 Engine 挑战

在 Add-to-App 中,最头疼的问题往往是 导航栈(Navigation Stack)。

比如:原生 A -> Flutter B -> 原生 C -> Flutter D

挑战

  1. 内存爆炸:如果每次 > Flutter 都创建一个新 Engine,内存会迅速耗尽。
  2. 状态丢失:如果复用同一个 Engine,从 C 返回 B 时,Flutter 的状态怎么恢复?

解决方案策略

当原生应用需要在 Feed 流中嵌入多个 Flutter 卡片,或者同时存在多个 Flutter 页面栈时,单纯的“单引擎”或“多引擎”都不够完美。

终极方案:FlutterEngineGroup 这是官方为了解决“多实例内存占用”推出的 API。

原理: 它允许你创建多个 Engine 实例,这些实例共享内存重的资源(如 Skia Shader、字体、Dart VM 快照),但保持 Dart Isolate 隔离

代码示例 (Android)

// 创建 EngineGroup
FlutterEngineGroup engineGroup = new FlutterEngineGroup(context);

// 创建第一个轻量级 Engine
FlutterEngine engine1 = engineGroup.createAndRunDefaultEngine(context);

// 创建第二个轻量级 Engine(复用资源,内存开销极低)
FlutterEngine engine2 = engineGroup.createAndRunDefaultEngine(context);

状态管理挑战: 由于 EngineGroup 中的 Isolate 是隔离的,engine1 中的全局变量无法被 engine2 直接读取。

  • 解决:相比于通过原生层(Host)作为中转站,或者使用持久化存储(Database/SharedPrefs)来同步不同 Flutter 页面间的数据。使用平台通道并且搭配上pigeon,相信会给你复杂原生交互提供不少的便利。

7. 常见问题与“避坑”指南

场景现象/原因解决方案
冷启动点击按钮后,等待 1-2 秒才出现 Flutter 画面,且有白屏。必须预热 Engine!在 App 启动时初始化 Engine 并存入 Cache。
调试运行原生 App 后,无法使用 Flutter 的热重载 (Hot Reload)。在终端运行 flutter attach,连接到正在运行的设备。
图片加载Flutter 无法加载原生 Assets 中的图片。原生图片需在 Flutter pubspec.yaml 中声明,或通过 Platform Channel 传递图片数据(字节流)。
生命周期Flutter 页面退后台后,代码被挂起。原生层需正确转发生命周期事件(lifecycle_channel),确保 Flutter 知道自己处于前台还是后台。

8. 总结

Add-to-App 方案打破了“非黑即白”的技术选型困境,是目前大型 App 引入 Flutter 的主流路径。

核心路径回顾:

  1. flutter create -t module 创建模块。
  2. 利用 FlutterEngineCache 解决性能问题。
  3. 利用 MethodChannel 打通数据经脉。

混合开发没有银弹,只有不断的权衡。希望本文能帮助你在现有的原生堡垒中,成功开辟出第一块 Flutter 的疆土!

延伸阅读

希望这篇分享对你有帮助!如果想了解更深层的 Engine 源码分析,欢迎留言讨论。