Flutter 混合开发

SegmentFault

共 8792字,需浏览 18分钟

 ·

2021-01-28 13:28

作者:xiangzhihong
来源:SegmentFault 思否社区




混合开发简介


使用 Flutter 从零开始开发App是一件轻松惬意的事情,但对于一些成熟的产品来说,完全摒弃原有 App 的历史沉淀,全面转向 Flutter 是不现实的。因此使用 Flutter 去统一 Android、iOS 技术栈,把它作为已有原生 App 的扩展能力,通过有序推进来提升移动终端的开发效率。


目前,想要在已有的原生 App 里嵌入一些 Flutter 页面主要有两种方案。一种是将原生工程作为 Flutter 工程的子工程,由 Flutter 进行统一管理,这种模式称为统一管理模式。另一种是将 Flutter 工程作为原生工程的子模块,维持原有的原生工程管理方式不变,这种模式被称为三端分离模式。




在 Flutter 框架出现早期,由于官方提供的混编方式以及资料有限,国内较早使用 Flutter 进行混合开发的团队大多使用的是统一管理模式。但是,随着业务迭代的深入,统一管理模式的弊端也随之显露,不仅三端(Android、iOS和Flutter)代码耦合严重,相关工具链耗时也随之大幅增长,最终导致开发效率降低。所以,后续使用 Flutter 进行混合开发的团队大多使用三端代码分离的模式来进行依赖治理,最终实现 Flutter 工程的轻量级接入。


除了可以轻量级接入外,三端代码分离模式还可以把 Flutter 模块作为原生工程的子模块,从而快速地接入 Flutter 模块,降低原生工程的改造成本。在完成对 Flutter 模块的接入后,Flutter 工程可以使用 Android Studio 进行开发,无需再打开原生工程就可以对 Dart 代码和原生代码进行开发调试。


使用三端分离模式进行 Flutter 混合开发的关键是抽离 Flutter 工程,将不同平台的构建产物依照标准组件化的形式进行管理,即 Android 使用 aar、iOS 使用 pod。也就是说,Flutter 的混编方案其实就是将 Flutter 模块打包成 aar 或者 pod 库,然后在原生工程像引用其他第三方原生组件库那样引入 Flutter 模块即可。




Flutter 模块


默认情况下,新创建的 Flutter 工程会包含 Flutter 目录和原生工程的目录。在这种情况下,原生工程会依赖 Flutter 工程的库和资源,并且无法脱离 Flutter 工程独立构建和运行。


在混合开发中,原生工程对 Flutter 的依赖主要分为两部分。一个是 Flutter 的库和引擎,主要包含 Flutter 的 Framework 库和引擎库;另一个是 Flutter 模块工程,即 Flutter 混合开发中的 Flutter 功能模块,主要包括 Flutter 工程 lib 目录下的 Dart 代码实现。


对于原生工程来说,集成 Flutter 只需要在同级目录创建一个 Flutter 模块,然后构建 iOS 和 Android 各自的 Flutter 依赖库即可。接下来,我们只需要在原生项目的同级目录下,执行 Flutter 提供的构建模块命令创建 Flutter 模块即可,如下所示。


flutter create -t module flutter_library


其中,flutter_library 为 Flutter 模块名。执行上面的命令后,会在原生工程的同级目录下生成一个 flutter_library 模块工程。Flutter 模块也是 Flutter 工程,使用 Android Studio 打开它,其目录如下图所示。



可以看到,和普通的 Flutter 工程相比,Flutter 模块工程也内嵌了 Android 工程和 iOS 工程,只不过默认情况下,Android 工程和 iOS 工程是隐藏的。因此,对于 Flutter 模块工程来说,也可以像普通工程一样使用 Android Studio 进行开发和调试。


同时,相比普通的 Flutter 工程,Flutter 模块工程的 Android 工程目录下多了一个 Flutter 目录,此目录下的 build.gradle 配置就是我们构建 aar 时的打包配置。同样,在 Flutter 模块工程的 iOS 工程目录下也会找到一个 Flutter 目录,这也是 Flutter 模块工程既能像 Flutter 普通工程一样使用 Android Studio 进行开发调试,又能打包构建 aar 或 pod 的原因。




Android 集成 Flutter


在原生 Android 工程中集成 Flutter,原生工程对 Flutter 的依赖主要包括两部分,分别是 Flutter 库和引擎,以及 Flutter 工程构建产物。


  • Flutter 库和引擎:包含 icudtl.dat、libFlutter.so 以及一些 class 文件,最终这些文件都会被封装到 Flutter.jar 中。
  • Flutter 工程产物:包括应用程序数据段 isolate_snapshot_data、应用程序指令段 isolate_snapshot_instr、虚拟机数据段 vm_snapshot_data、虚拟机指令段 vm_snapshot_instr 以及资源文件 flutter_assets。

和原生 Android 工程集成其他插件库的方式一样,在原生 Android 工程中引入 Flutter 模块需要先在 settings.gradle 中添加如下代码。

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

其中,flutter_library 为我们创建的 Flutter 模块。然后,在原生 Android 工程的 app 目录的 build.gradle 文件中添加如下依赖。

dependencies {
    implementation project(":flutter")
}

然后编译并运行原生 Android 工程,如果没有任何错误则说明集成 Flutter 模块成功。需要说明的是,由于Flutter 支持的最低版本为16,所以需要将 Android 项目的 minSdkVersion 修改为16。

如果出现“程序包 android.support.annotation 不存在”的错误,需要使用如下的命令来创建 Flutter 模块,因为最新版本的 Android 默认使用 androidx 来管理包。

flutter create --androidx -t module flutter_library

对于 Android 原生工程,如果还没有升级到 androidx,可以在原生 Android 工程上右键,然后依次选择【Refactor】→【Migrate to Androidx】将 Android 工程升级到 androidx 包管理。

在原生 Android 工程中成功添加 Flutter 模块依赖后,打开原生 Android 工程,并在应用的入口 MainActivity 文件中添加如下代码。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View flutterView = Flutter.createView(this, getLifecycle(), "route1");
        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        addContentView(flutterView, layoutParams);
    }
}

通过 Flutter 提供的 createView() 方法,可以将 Flutter 页面构建成 Android 能够识别的视图,然后将这个视图使用 Android 提供的 addContentView() 方法添加到父窗口即可。重新运行原生 Android 工程,最终效果如下图所示。



如果原生 Android 的 MainActivity 加载的是一个 FrameLayout,那么加载只需要将 Flutter 页面构建成一个 Fragment 即可,如下所示。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FragmentTransaction ft= getSupportFragmentManager().beginTransaction();
        ft.replace(R.id.fragment_container, Flutter.createFragment("Hello Flutter"));
        ft.commit();
    }
}

除了使用 Flutter 模块方式集成外,还可以将 Flutter 模块打包成 aar,然后再添加依赖。在 flutter_library 根目录下执行 aar 打包构建命令即可抽取 Flutter 依赖,如下所示。

flutter build apk --debug

此命令的作用是将 Flutter 库和引擎以及工程产物编译成一个 aar 包,上面命令编译的 aar 包是 debug 版本,如果需要构建 release 版本,只需要把命令中的 debug 换成 release 即可。

打包构建的 flutter-debug.aar 位于 .android/Flutter/build/outputs/aar/ 目录下,可以把它拷贝到原生 Android 工程的 app/libs 目录下,然后在原生 Android 工程的 app 目录的打包配置 build.gradle 中添加对它的依赖,如下所示。

dependencies {
  implementation(name: 'flutter-debug', ext: 'aar')   
}

然后重新编译一下项目,如果没有任何错误提示则说明 Flutter 模块被成功集成到 Android 原生工程中。



iOS 集成 Flutter


原生 iOS 工程对 Flutter 的依赖包含 Flutter 库和引擎,以及 Flutter 工程编译产物。其中,Flutter 库和引擎指的是 Flutter.framework 等,Flutter 工程编译产物指的是 App.framework 等。

在原生 iOS 工程中集成 Flutter 需要先配置好 CocoaPods,CocoaPods 是 iOS 的类库管理工具,用来管理第三方开源库。在原生 iOS 工程中执行 pod init 命令创建一个 Podfile 文件,然后在 Podfile 文件中添加 Flutter 模块依赖,如下所示。

flutter_application_path = '../flutter_ library/
load File.join(flutter_application_path, '
.ios', 'Flutter', 'podhelper.rb')

target '
iOSDemo' do
  # Comment the next line if you don'
t want to use dynamic frameworks
  use_frameworks!
  install_all_flutter_pods(flutter_application_path)

  # Pods for iOSDemo
  … //省略其他脚本
end '

然后,关闭原生 iOS 工程,并在原生 iOS 工程的根目录执行 pod install 命令安装所需的依赖包。安装完成后,使用 Xcode 打开 iOSDemo.xcworkspace 原生工程。

默认情况下,Flutter 是不支持 Bitcode 的,Bitcode 是一种 iOS 编译程序的中间代码,在原生 iOS 工程中集成 Flutter 需要禁用 Bitcode。在 Xcode 中依次选择【TAGETS】→【Build Setttings】→【Build Options】→【Enable Bitcode】来禁用 Bitcode,如下图所示。



如果使用的是 Flutter 早期的版本,还需要添加 build phase 来支持构建 Dart 代码。依次选择【TAGGETS】→【Build Settings】→【Enable Phases】,然后点击左上角的加号新建一个“New Run Script Phase”,添加如下脚本代码。

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed

不过,最新版本的Flutter已经不需要再添加脚本了。重新运行原生iOS工程,如果没有任何错误则说明iOS成功集成Flutter模块。

除了使用Flutter模块方式外,还可以将Flutter模块打包成可以依赖的动态库,然后再使用CocoaPods添加动态库。首先,在flutter_library根目录下执行打包构建命令生成framework动态库,如下所示。

flutter build ios --debug

上面命令是将 Flutter 工程编译成 Flutter.framework 和 App.framework 动态库。如果要生成 release 版本,只需要把命令中的 debug 换成 release 即可。

然后,在原生 iOS 工程的根目录下创建一个名为 FlutterEngine 的目录,并把生成的两个framework 动态库文件拷贝进去。不过,iOS 生成模块化产物要比 Android 多一个步骤,因为需要把 Flutter 工程编译生成的库手动封装成一个 pod。首先,在 flutter_ library 该目录下创建 FlutterEngine.podspec,然后添加如下脚本代码。

Pod::Spec.new do |s|
  s.name             = 'FlutterEngine'
  s.version          = '0.1.0'
  s.summary          = 'FlutterEngine'
  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           = { 'xzh' => '1044817967@qq.com' }
  s.source       = { :git => "", :tag => "#{s.version}" }
  s.ios.deployment_target = '9.0'
  s.ios.vendored_frameworks = 'App.framework''Flutter.framework'
end

然后,执行 pod lib lint 命令即可拉取 Flutter 模块所需的组件。接下来,在原生 iOS 工程的 Podfile 文件添加生成的库即可。

target 'iOSDemo' do
    pod 'FlutterEngine', :path => './'
end

重新执行 pod install 命令安装依赖库,原生 iOS 工程集成 Flutter 模块就完成了。接下来,使用 Xcode 打开 ViewController.m 文件,然后添加如下代码。

#import "ViewController.h"
#import 
#import 

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton *button = [[UIButton alloc]init];
    [button setTitle:@"加载Flutter模块" forState:UIControlStateNormal];
    button.backgroundColor=[UIColor redColor];
    button.frame = CGRectMake(50, 50, 200, 100);
    [button setTitleColor:[UIColor redColor] forState:UIControlStateHighlighted];
    [button addTarget:self action:@selector(buttonPrint) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

- (void)buttonPrint{
    FlutterViewController * flutterVC = [[FlutterViewController alloc]init];
    [flutterVC setInitialRoute:@"defaultRoute"];
    [self presentViewController:flutterVC animated:true completion:nil];
}

@end

在上面的代码中,我们在原生 iOS 中创建了一个按钮,点击按钮时就会跳转到 Flutter 页面,最终效果如下图所示。



默认情况下,Flutter 为提供了两种调用方式,分别是 FlutterViewController 和FlutterEngine。对于 FlutterViewController 来说,打开 ViewController.m 文件,在里面添加一个加载 flutter 页面的方法并且添加一个按钮看来调用。



Flutter 模块调试


众所周知,Flutter 的优势之一就是在开发过程中使用热重载功能来实现快速调试。默认情况下,在原生工程中集成 Flutter 模块后热重载功能是失效的,需要重新运行原生工程才能看到效果。如此一来,Flutter 开发的热重载优势就失去了,并且开发效率也随之降低。

那么,能不能在混合项目中开启 Flutter 的热重载呢?答案是可以的,只需要经过如下步骤即可开启热重载功能。首先,关闭原生应用,此处所说的关闭是指关闭应用的进程,而不是简单的退出应用。在 Flutter 模块的根目录中输入 flutter attach 命令,然后再次打开原生应用,就会看到连接成功的提示,如下图所示。



如果同时连接了多台设备,可以使用 flutter attach -d 命令来指定连接的设备。接下来,只需要按r键即可执行热重载,按R键即可执行热重启,按d键即可断开连接。

在 Flutter 工程中,我们可以直接点击debug按钮来进行代码调试,但在混合项目中,直接点击 debug 按钮是不起作用的。此时,可以使用 Android Studio 提供的 flutter attach 按钮来建立与 flutter 模块的连接,进行实现对 flutter 模块的代码调试,如图下图所示。



上面只是完成了在原生工程中引入 Flutter 模块,具体开发时还会遇到与 Flutter 模块的通信问题、路由管理问题,以及打包等。



点击左下角阅读原文,到 SegmentFault 思否社区 和文章作者展开更多互动和交流,或扫描下方二维码添加“ SF 思否小姐姐 ”,回复“ 入群 ”即可加入我们的技术交流群,收获更多的技术文章~

- END -

浏览 18
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报