flutter 专题 八十八 Flutter原生混合开发

使用 Flutter 从头开始写一个 App是一件轻松惬意的事情。但是对于成熟产品来说,完全摒弃原有 App 的历史沉淀,全面转向 Flutter 并不现实。用 Flutter 去统一 iOS/Android 技术栈,把它作为已有原生 App 的扩展,然后通过逐步试验有序推进从而提升终端开发效率,可能才是现阶段 Flutter 最有效的集成方式。

那么,Flutter 工程与原生工程该如何组织管理?不同平台的 Flutter 工程打包构建产物该如何抽取封装?封装后的产物该如何引入原生工程?原生工程又该如何使用封装后的 Flutter 能力?

这些问题使得在已有原生 App 中接入 Flutter 看似并不是一件容易的事情。那接下来,我就和你介绍下如何在原生 App 中以最自然的方式接入 Flutter。

准备工作

既然要在原生应用中混编 Flutter,相信你一定已经准备好了原生应用工程。如果你还没有准备好也没关系,我会以一个最小化的示例和你演示这个改造过程。

首先,我们分别用 Xcode 与 Android Studio 快速建立一个只有首页的基本工程,工程名分别为 iOSDemo 与 AndroidDemo。

到此,Android 工程就已经准备好了;而对于 iOS 工程来说,由于基本工程并不支持以组件化的方式管理项目,因此我们还需要多做一步,将其改造成使用 CocoaPods 管理的工程,也就是要在 iOSDemo 根目录下创建一个只有基本信息的 Podfile 文件。Podfile文件的配置如下:

复制代码
use_frameworks!
platform :ios, '8.0'
target 'iOSDemo' do
#todo
end

然后,在命令行输入 pod install 命令后,会自动生成一个 iOSDemo.xcworkspace 文件,该文件存放的就是我们项目需要的依赖库,这时我们就完成了 iOS 工程改造。

混编方案

如果你想要在已有的原生 App 里嵌入一些 Flutter 页面,有两个办法可以实现,即统一管理模式和三端分离模式。

  • 将原生工程作为 Flutter 工程的子工程,由 Flutter 统一管理。这种模式,就是统一管理模式。
  • 将 Flutter 工程作为原生工程共用的子模块,维持原有的原生工程管理方式不变。这种模式,就是三端分离模式。

由于 Flutter 早期提供的混编方式能力及相关资料有限,国内较早使用 Flutter 混合开发的团队大多使用的是统一管理模式。但是,随着功能迭代的深入,这种方案的弊端也随之显露,不仅三端(Android、iOS、Flutter)代码耦合严重,相关工具链耗时也随之大幅增长,导致开发效率降低。

所以,后续使用 Flutter 混合开发的团队陆续按照三端代码分离的模式来进行依赖治理,实现了 Flutter 工程的轻量级接入。

除此之外,三端代码分离模式还可以把 Flutter 模块作为原生工程的子模块,从而快速实现 Flutter 功能的"热插拔",降低原生工程改造的成本。而 Flutter 工程通过 Android Studio 进行管理,无需打开原生工程,可直接进行 Dart 代码和原生代码的开发调试。

三端工程分离模式的关键是抽离 Flutter 工程,将不同平台的构建产物依照标准组件化的形式进行管理,即 Android 使用 aar、iOS 使用 pod。换句话说,接下来介绍的混编方案会将 Flutter 模块打包成 aar 和 pod,这样原生工程就可以像引用其他第三方原生组件库那样快速接入 Flutter 了。

集成Flutter准备

当我们创建一个新的Flutter 工程时,除了一些通用配置外,Flutter还包括 Flutter 工程和原生工程的目录(即 iOS 和 Android 两个目录)。在这种情况下,原生工程就会依赖于 Flutter 相关的库和资源,从而无法脱离父目录进行独立构建和运行。

原生工程对 Flutter 的依赖主要分为两部分:

  • Flutter 库和引擎,也就是 Flutter 的 Framework 库和引擎库;
  • Flutter 工程,也就是我们自己实现的 Flutter 模块功能,主要包括 Flutter 工程 lib 目录下的 Dart 代码实现的这部分功能。

在已经有原生工程的情况下,我们需要在同级目录创建 Flutter 模块,构建 iOS 和 Android 各自的 Flutter 依赖库。这也很好实现,Flutter 就为我们提供了这样的命令。我们只需要在原生项目的同级目录下,执行 Flutter 命令创建名为 flutter_library 的模块即可,命令如下。

复制代码
Flutter create -t module flutter_library

这里的 Flutter 模块,也是 Flutter 工程,我们用 Android Studio 打开它,其目录如下图所示。

可以看到,和传统的 Flutter 工程相比,Flutter 模块工程也有内嵌的 Android 工程与 iOS 工程,因此我们可以像普通工程一样使用 Android Studio 进行开发调试。

仔细查看可以发现,Flutter 模块有一个细微的变化:Android 工程下多了一个 Flutter 目录,这个目录下的 build.gradle 配置就是我们构建 aar 的打包配置。这就是模块工程既能像 Flutter 传统工程一样使用 Android Studio 开发调试,又能打包构建 aar 与 pod 的秘密。

实际上,iOS 工程的目录结构也有细微变化,但这个差异并不影响打包构建,因此此处就不再展开了。

然后,我们打开 main.dart 文件,将其逻辑更新为以下代码逻辑,即一个写着"Hello from Flutter"的全屏红色的 Flutter Widget,如下所示。

复制代码
import 'package:flutter/material.dart';
import 'dart:ui';

void main() => runApp(_widgetForRoute(window.defaultRouteName));//独立运行传入默认路由

Widget _widgetForRoute(String route) {
  switch (route) {
    default:
      return MaterialApp(
        home: Scaffold(
          backgroundColor: const Color(0xFFD63031),//ARGB红色
          body: Center(
            child: Text(
              'Hello from Flutter', //显示的文字
              textDirection: TextDirection.ltr,
              style: TextStyle(
                fontSize: 20.0,
                color: Colors.blue,
              ),
            ),
          ),
        ),
      );
  }
}

我们创建的 Widget 实际上是包在一个 switch-case 语句中的。这是因为封装的 Flutter 模块一般会有多个页面级 Widget,原生 App 代码则会通过传入路由标识字符串,告诉 Flutter 究竟应该返回何种 Widget。为了简化案例,在这里我们忽略标识字符串,统一返回一个 MaterialApp。

接下来,我们要做的事情就是把这段代码编译打包,构建出对应的 Android 和 iOS 依赖库,实现原生工程的接入。

现在,我们首先来看看 Android 工程如何接入。

Android原生集成

之前我们提到原生工程对 Flutter 的依赖主要分为两部分,对应到 Android 平台,这两部分分别是:

  • Flutter 库和引擎,也就是 icudtl.dat、libFlutter.so,还有一些 class 文件。这些文件都封装在 Flutter.jar 中。
  • Flutter 工程产物,主要包括应用程序数据段 isolate_snapshot_data、应用程序指令段 isolate_snapshot_instr、虚拟机数据段 vm_snapshot_data、虚拟机指令段 vm_snapshot_instr和资源文件 Flutter_assets等内容。

搞清楚 Flutter 工程的 Android 编译产物之后,我们需要对 Android 的 Flutter 依赖进行抽取,步骤如下。

首先,在 Flutter_library 的根目录下,执行 aar 打包构建命令,如下所示。

复制代码
Flutter build apk --debug

这条命令的作用是编译工程产物,并将 Flutter.jar 和工程产物编译结果封装成一个 aar,如下图所示。

你很快就会想到,如果是构建 release 产物,只需要把 debug 换成 release 就可以了。

打包构建的 flutter-debug.aar 位于.android/Flutter/build/outputs/aar/ 目录下,我们把它拷贝到原生 Android 工程 AndroidDemo 的 app/libs 目录下,并在 App 的打包配置 build.gradle 中添加对它的依赖。

复制代码
...
repositories {
    flatDir {
        dirs 'libs'   // aar目录
    }
}
android {
    ...
    compileOptions {
        sourceCompatibility 1.8 //Java 1.8
        targetCompatibility 1.8 //Java 1.8
    }
    ...
}

dependencies {
    ...
    implementation(name: 'flutter-debug', ext: 'aar')//Flutter模块aar
    ...
}

Sync 一下项目,Flutter 模块就被添加到了 Android 项目中。

然后,我们试着改一下 MainActivity.java 的代码,把它的 contentView 改成 Flutter 的 widget,如下所示。

复制代码
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    View FlutterView = Flutter.createView(this, getLifecycle(), "defaultRoute"); //传入路由标识符
    setContentView(FlutterView);//用FlutterView替代Activity的ContentView
}

重新运行Android原生工程,效果如下图。

iOS 原生集成

iOS 工程接入的情况要稍微复杂一些。在 iOS 平台,原生工程对 Flutter 的依赖分别是:

  • Flutter 库和引擎,即 Flutter.framework;
  • Flutter 工程的产物,即 App.framework。

iOS 平台的 Flutter 模块抽取,实际上就是通过打包命令生成这两个产物,并将它们封装成一个 pod 供iOS原生工程引用。

类似地,首先我们在 Flutter_library 的根目录下,执行 iOS 打包构建命令。

复制代码
Flutter build ios --debug

这条命令的作用是编译 Flutter 工程生成两个产物:Flutter.framework 和 App.framework。同样,把 debug 换成 release 就可以构建 release 产物(当然,你还需要处理一下签名问题)。

然后,在 iOSDemo 的根目录下创建一个名为 FlutterEngine 的目录,并把这两个 framework 文件拷贝进去。iOS 的模块化产物工作要比 Android 多一个步骤,因为我们需要把这两个产物手动封装成 pod。因此,我们还需要在该目录下创建 FlutterEngine.podspec,即 Flutter 模块的组件定义。

复制代码
Pod::Spec.new do |s|
  s.name             = 'FlutterEngine'
  s.version          = '0.1.0'
  s.summary          = 'XXXXXXX'
  s.description      = <<-DESC
TODO: Add long description of the pod here.
                       DESC
  s.homepage         = 'https://github.com/xx/FlutterEngine'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'chenhang' => 'hangisnice@gmail.com' }
  s.source       = { :git => "", :tag => "#{s.version}" }
  s.ios.deployment_target = '8.0'
  s.ios.vendored_frameworks = 'App.framework', 'Flutter.framework'
end

然后,执行pod lib lint 命令,Flutter 模块组件就已经做好了。接下来,我们再修改 Podfile 文件把它集成到 iOSDemo 工程中,添加如下脚本。

复制代码
...
target 'iOSDemo' do
    pod 'FlutterEngine', :path => './'
end

然后,执行pod install 命令,Flutter 模块就集成进 iOS 原生工程中了。再次,我们试着修改一下 AppDelegate.m 的代码,把 window 的 rootViewController 改成 FlutterViewController,如下所示。

复制代码
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    FlutterViewController *vc = [[FlutterViewController alloc]init];
    [vc setInitialRoute:@"defaultRoute"]; //路由标识符
    self.window.rootViewController = vc;
    [self.window makeKeyAndVisible];
    return YES;
}

最后运行原生项目,一个写着"Hello from Flutter"的全屏红色的 Flutter Widget 也展示出来了,如下图所示。

总结

在原生工程中集成Flutter是现阶段最常见的方式。通过分离 Android、iOS 和 Flutter 三端工程,抽离 Flutter 库和引擎及工程代码为组件库,以 Android 和 iOS 平台最常见的 aar 和 pod 形式接入原生工程,从而将不同平台的构建产物依照标准组件化的形式进行管理。

如果每次通过构建 Flutter 模块工程,都是手动搬运 Flutter 编译产物,那很容易就会因为工程管理混乱导致 Flutter 组件库被覆盖,从而引发难以排查的 Bug。而要解决此类问题的话,我们可以引入 CI 自动构建框架,把 Flutter 编译产物构建自动化,原生工程通过接入不同版本的构建产物,实现更优雅的三端分离模式。

相关推荐
0wioiw06 小时前
Flutter基础(FFI)
flutter
Georgewu9 天前
【HarmonyOS 5】鸿蒙跨平台开发方案详解(一)
flutter·harmonyos
爱吃鱼的锅包肉9 天前
Flutter开发中记录一个非常好用的图片缓存清理的插件
flutter
守城小轩10 天前
Chromium 136 编译指南 macOS篇:编译优化技巧(六)
macos·策略模式
Areslee10 天前
一种通用跨平台实现SEH的解决方案
linux·macos·内核·跨平台·seh
张风捷特烈10 天前
每日一题 Flutter#13 | build 回调的 BuildContext 是什么
android·flutter·面试
恋猫de小郭10 天前
Flutter 又双叒叕可以在 iOS 26 的真机上 hotload 运行了,来看看又是什么黑科技
android·前端·flutter
麦兜*10 天前
【node】Mac m1 安装nvm 和node
java·前端·vue.js·chrome·macos·vue·nvm
QC七哥10 天前
跨平台开发flutter初体验
android·flutter·安卓·桌面开发