阅读视图

发现新文章,点击刷新页面。

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

为什么我们需要 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 源码分析,欢迎留言讨论。

❌