Flutter 开发之 Native 集成 Flutter 混合开发

本文先介绍一下现有工程如何集成 Flutter 实现混合开发,以及混合项目如何打包,再探索下如何下降原生和 Flutter 之间的依赖,使 Flutter 开发对原生开发的影响尽可能下降,以及一些我在尝试中遇到的问题及解决。android

介绍 Flutter

Flutter 是 Google 发布的一个用于建立跨平台、高性能移动应用的框架。Flutter 和 QT mobile 同样,都没有使用原生控件,相反都实现了一个自绘引擎,使用自身的布局、绘制系统。开发者能够经过 Dart 语言开发 App,一套代码同时运行在 iOS 和 Android平台。Flutter 提供了丰富的组件、接口,开发者能够很快地为 Flutter 添加 Native 扩展。ios

前提工做

开发者须要安装好 Flutter 的环境,执行flutter doctor -v验证。git

flutter_doctor_vgithub

 

验证经过后便可开始集成 Flutter。web

现有原生工程集成 Flutter

最官方的教程应该是Add Flutter to existing apps了,按照教程以下一步步操做:shell

1.建立 flutter modulexcode

使用flutter create xxx指令建立的 Flutter 项目包括用于 Flutter/Dart 代码的很是简单的工程。你能够修改 main.dart 的内容,以知足你的须要,并在此基础上进行构建。浏览器

假设你有一个已经存在 iOS 工程(以 flutterHybridDemo 为例)在some/path/flutterHybridDemo,那么你新建的 flutter_module 和 iOS 工程应该在同一目录下(即都在 path 下)。网络

$ cd some/path/
$ flutter create -t module flutter_module

flutter_module目录结构app

经过shift+command+.显示/隐藏隐藏文件夹

  • lib/main.dart:存放的是 Dart 语言编写的代码,这里是核心代码;
  • pubspec.yaml:配置依赖项的文件,好比配置远程 pub 仓库的依赖库,或者指定本地资源(图片、字体、音频、视频等);
  • .ios/:iOS 部分代码;
  • .android/:Android 部分代码;
  • build/:存储 iOS 和 Android 构建文件;
  • test/:测试代码。

2.将 flutter module 做为依赖添加到工程

假设文件夹结构以下:

some/path/
  flutter_module/
    lib/main.dart
    .ios/
    ...
  flutterHybridDemo/
    flutterHybridDemo.xcodeproj
    flutterHybridDemo/
        AppDelegate.h
        AppDelegate.m
        ...

集成 Flutter 框架须要使用CocoaPods,这是由于 Flutter 框架还须要对 flutter_module 中可能包含的任何 Flutter 插件可用。

- 若是须要,请参考cocoapods.org了解如何在您的电脑上安装 CocoaPods。

建立 Podfile:

$ cd some/path/flutterHybridDemo
$ pod init

此时工程中会出现一个 Podfile 文件,添加项目依赖的第三方库就在这个文件中配置,编辑 Podfile 文件添加最后两行代码:

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'TestOne' do
  # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
  # use_frameworks!

  # Pods for TestOne

  target 'TestOneTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'TestOneUITests' do
    inherit! :search_paths
    # Pods for testing
  end

end

#新添加的代码
flutter_application_path = '../flutter_module'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)

- 若是你的工程(flutterHybridDemo)已经在使用 Cocoapods ,你只须要作如下几件事来整合你的 flutter_module 应用程序:

(1)添加以下内容到 Podfile:

flutter_application_path = '../flutter_module'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)

(2)执行pod install

当你在some/path/flutter_module/pubspec.yaml中修改 Flutter 插件依赖时,须要先执行flutter packages get经过 podhelper.rb 脚原本刷新插件列表,而后再从some/path/flutterHybridDemo执行一次pod install

podhelper.rb 脚本将确保你的插件和 Flutter 框架被添加到你的工程中,以及 bitcode 被禁用。

(3)禁用 bitcode

由于 Flutter 如今不支持 bitcode。须要设置 Build Settings->Build Options->Enable Bitcode 为 NO。

 

bitcode 禁用

3.为编译 Dart 代码配置 build phase

打开 iOS 工程,选中项目的 Build Phases 选项,点击左上角+号按钮,选择 New Run Script Phase。

 

配置 build phase

将下面的 shell 脚本添加到输入框中:

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

最后,确保 Run Script 这一行在 "Target dependencies" 或者 "Check Pods Manifest.lock" 后面。

 

配置 build phase

至此,你能够编译一下工程确保无误:⌘B

4.在 iOS 工程中使用 FlutterViewController

首先声明你的 AppDelegate 是 FlutterAppDelegate 的子类。而后定义一个 FlutterEngine 属性,它能够帮助你注册一个没有 FlutterViewController 实例的插件。

在 AppDelegate.h:

#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>

@interface AppDelegate : FlutterAppDelegate
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end

在AppDelegate.m,修改didFinishLaunchingWithOptions方法以下:

#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins
#include "AppDelegate.h"

@implementation AppDelegate

// This override can be omitted if you do not have any Flutter Plugins.
- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];
  [self.flutterEngine runWithEntrypoint:nil];
  [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

若是 AppDelegate 已经继承于别的类的时候,能够经过让你的 delegate 实现FlutterAppLifeCycleProvider协议:

#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins

@interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>
@property (strong, nonatomic) UIWindow *window;
@end

而后生命周期方法应该由 FlutterPluginAppLifeCycleDelegate 来代理:

@implementation AppDelegate
{
    FlutterPluginAppLifeCycleDelegate *_lifeCycleDelegate;
}

- (instancetype)init {
    if (self = [super init]) {
        _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
    }
    return self;
}

- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
    [GeneratedPluginRegistrant registerWithRegistry:self]; // Only if you are using Flutter plugins.
    return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}

// Returns the key window's rootViewController, if it's a FlutterViewController.
// Otherwise, returns nil.
- (FlutterViewController*)rootFlutterViewController {
    UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
    if ([viewController isKindOfClass:[FlutterViewController class]]) {
        return (FlutterViewController*)viewController;
    }
    return nil;
}

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
    [super touchesBegan:touches withEvent:event];

    // Pass status bar taps to key window Flutter rootViewController.
    if (self.rootFlutterViewController != nil) {
        [self.rootFlutterViewController handleStatusBarTouches:event];
    }
}

- (void)applicationDidEnterBackground:(UIApplication*)application {
    [_lifeCycleDelegate applicationDidEnterBackground:application];
}

- (void)applicationWillEnterForeground:(UIApplication*)application {
    [_lifeCycleDelegate applicationWillEnterForeground:application];
}

- (void)applicationWillResignActive:(UIApplication*)application {
    [_lifeCycleDelegate applicationWillResignActive:application];
}

- (void)applicationDidBecomeActive:(UIApplication*)application {
    [_lifeCycleDelegate applicationDidBecomeActive:application];
}

- (void)applicationWillTerminate:(UIApplication*)application {
    [_lifeCycleDelegate applicationWillTerminate:application];
}

- (void)application:(UIApplication*)application
didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings {
    [_lifeCycleDelegate application:application
didRegisterUserNotificationSettings:notificationSettings];
}

- (void)application:(UIApplication*)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
    [_lifeCycleDelegate application:application
didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}

- (void)application:(UIApplication*)application
didReceiveRemoteNotification:(NSDictionary*)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
    [_lifeCycleDelegate application:application
       didReceiveRemoteNotification:userInfo
             fetchCompletionHandler:completionHandler];
}

- (BOOL)application:(UIApplication*)application
            openURL:(NSURL*)url
            options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
    return [_lifeCycleDelegate application:application openURL:url options:options];
}

- (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url {
    return [_lifeCycleDelegate application:application handleOpenURL:url];
}

- (BOOL)application:(UIApplication*)application
            openURL:(NSURL*)url
  sourceApplication:(NSString*)sourceApplication
         annotation:(id)annotation {
    return [_lifeCycleDelegate application:application
                                   openURL:url
                         sourceApplication:sourceApplication
                                annotation:annotation];
}

- (void)application:(UIApplication*)application
performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
  completionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) {
    [_lifeCycleDelegate application:application
       performActionForShortcutItem:shortcutItem
                  completionHandler:completionHandler];
}

- (void)application:(UIApplication*)application
handleEventsForBackgroundURLSession:(nonnull NSString*)identifier
  completionHandler:(nonnull void (^)(void))completionHandler {
    [_lifeCycleDelegate application:application
handleEventsForBackgroundURLSession:identifier
                  completionHandler:completionHandler];
}

- (void)application:(UIApplication*)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
    [_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];
}

- (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate {
    [_lifeCycleDelegate addDelegate:delegate];
}
@end

在 ViewController 中添加跳转到 FlutterViewController 的测试代码便可:

#import "ViewController.h"
#import <Flutter/Flutter.h>
#import "AppDelegate.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button addTarget:self
               action:@selector(handleButtonAction)
     forControlEvents:UIControlEventTouchUpInside];
    [button setTitle:@"Jump to flutterViewController" forState:UIControlStateNormal];
    [button setBackgroundColor:[UIColor grayColor]];
    button.frame = CGRectMake(80.0, 210.0, 300.0, 40.0);
    button.center = self.view.center;
    [self.view addSubview:button];
}

- (void)handleButtonAction {
    AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
    FlutterEngine *flutterEngine = delegate.flutterEngine;
    
    FlutterViewController *flutterVC = [[FlutterViewController alloc]initWithEngine:flutterEngine nibName:nil bundle:nil];
    [self presentViewController:flutterVC animated:YES completion:nil];
}
@end

5.使用热重载的方式调试 Dart 代码

热重载指的是不用从新启动就看到修改后的效果,相似 web 网页开发时保存就看到效果的方式。
进入 flutter module,在终端执行命令:

$ cd some/path/flutter_module
$ flutter run

flutter run

而且你能在控制台中看下以下内容:

?  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on iPhone X is available at: http://127.0.0.1:54741/
For a more detailed help message, press "h". To quit, press "q".

你能够在 flutter_module 中编辑 Dart code,而后在终端输入 r 来使用热重载。你也能够在浏览器中输入上面的 URL 来查看断点、分析内存和其余的调试任务。

集成 Flutter 后工程打包

1. flutter build ios

执行flutter build ios以建立 release 版本(flutter build 默认为--release,如需建立 debug 版本执行flutter build ios —debug)。

2.成功后修改 Xcode 为 release 模式配置

3.最后选择 Product > Archive 以生成构建版本便可

archive 成功

混合工程改造优化

Flutter 的工程结构比较特殊,由 Flutter 目录、Native 工程的目录(即 iOS 和 Android 两个目录)组成。默认状况下,引入了 Flutter 的 Native 工程没法脱离父目录进行独立构建和运行,由于它会反向依赖于 Flutter 相关的库和资源。

实际上,在真实的开发状况下,开发者不多会建立一个彻底 Flutter 的工程重写项目,更多的状况是原生工程集成 Flutter。

1.问题

这样就带来了一系列问题:

(1)构建打包问题:引入 Flutter 后,Native 工程因对其有了依赖和耦合,从而没法独立编译构建。在 Flutter 环境下,工程的构建是从 Flutter 的构建命令开始,执行过程当中包含了 Native 工程的构建,开发者要配置完整的 Flutter 运行环境才能走通整个流程

(2)混合编译带来的开发效率的下降:在转型 Flutter 的过程当中必然有许多业务仍使用 Native 进行开发,工程结构的改动会使开发没法在纯 Native 环境下进行,而适配到 Flutter 工程结构对纯 Native 开发来讲又会形成没必要要的构建步骤,形成开发效率的下降。

2.目标

但愿能将 Flutter 依赖抽取出来,做为一个 Flutter 依赖库,供纯 Native 工程引用,无需配置完整的 Flutter 环境。

3.Flutter 产物

iOS 工程对 Flutter 有以下依赖:

  • Flutter.framework:Flutter 库和引擎

  • App.framework:dart 业务源码相关文件

  • flutter_assets:Flutter依赖的静态资源,如字体,图片等

  • Flutter Plugin:编译出来的各类 plugin 的 framework

把以上依赖的编译结果抽取出来,便是 Flutter 相关代码的最终产物。

那么咱们只须要将这些打包成一个 SDK 依赖的形式提供给 Native 工程,就能够解除 Native 工程对 Flutter 工程的直接依赖。

产物的产生:

对 flutter 工程执行 flutter build 命令后,生成在.ios/Flutter目录下,直接手动拷贝 framework 到主工程便可。

注意事项:

framework 选择 Create groups 加入文件夹,flutter_assets 选择 Create folder references 加入文件夹。

add_in_project

加入完成后的结构:

thirdFramework

framework 加入后,记住必定要确认 framework 已在 TARGETS -> General -> Embedded Binaries 中添加完成。

embedded_binaires

最后改造 APPDelegate 便可:

#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>

@interface AppDelegate : FlutterAppDelegate <UIApplicationDelegate>

@property (strong, nonatomic) FlutterEngine *flutterEngine;

@end
#import "AppDelegate.h"

@interface AppDelegate ()
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.flutterEngine = [[FlutterEngine alloc]initWithName:@"io.flutter" project:nil];
    [self.flutterEngine runWithEntrypoint:nil];
    return YES;
}

4. 优化

为了更方便管理 framework,能够将这些文件上传到远程仓库,经过 CocoaPods 导入,Native 项目只需及时更新 pod 依赖便可。

我遇到过的一些问题及解决

1.在 Android Studio 上跑设备

More than one device connected; please specify a device with the '-d <deviceId>' flag, or use '-d all' to act on all devices.

选择模拟器

提示你当前有两个模拟器设备,跑设备的时候要选择运行在哪一个设备上,flutter run后面拼接上“-d <deviceId>”,deviceId 是第二列的内容。

flutter run -d emulator-5554
flutter run -d C517D2D4-EAFA-42CA-B260-A18FA0ABFF60

电脑连着真机也同理,改为真机的 deviceId 便可。

2.flutter build ios 报错

build 时可能遇到的错误:

It appears that your application still contains the default signing identifier.Try replacing 'com.example' with your signing id in Xcode:

open ios/Runner.xcworkspace

build 时可能遇到的错误

解决方法:

修改some/flutter_module/.ios/下 Runner 工程的 Bundle Identifier 和原生工程的一致,再次运行flutter build ios便可。

3.开发时打包产物编译失败

当你用flutter build ios的产物添加到原生工程中,跳转到 Flutter 界面会黑屏并报出以下错误:

flutter_build_questions

Failed to find snapshot: …/Library/Developer/CoreSimulator/Devices/…/data/Containers/Bundle/Application/…/FlutterMixDemo.app/Frameworks/App.framework/flutter_assets/kernel_blob.bin

如何解决:

调试模式下用flutter build ios —debug的产物,再次拖入工程便可。

缘由:

首先咱们对比下,执行flutter build ios和执行flutter build ios --debug后 .ios/Flutter/App.framework/flutter_assets的文件内容:

flutter_build_ios.png

 

flutter_build_ios_debug.png

能够发现,差异是在于三个文件:isolate_snapshot_data、kernel_blob.bin、vm_snapshot_data。

这里涉及 Flutter 的编译模式知识,具体能够参阅Flutter 的两种编译模式

Flutter 开发阶段的编译模式:使用了 Kernel Snapshot 模式编译,打包产物中,能够发现几样东西:

  • isolate_snapshot_data:用于加速 isolate 启动,业务无关代码,固定,仅和 flutter engine 版本有关;

  • platform.dill:和 Dart VM 相关的 kernel 代码,仅和 Dart 版本以及 engine 编译版本有关。固定,业务无关代码;

  • vm_snapshot_data:用于加速 Dart VM 启动的产物,业务无关代码,仅和 flutter engine 版本有关;

  • kernel_blob.bin:业务代码产物 。

Flutter 生产阶段的编译模式:选择了 AOT 打包。

4.集成后 Native 工程报错

Shell Script Invocation Error

line 2:/packages/flutter_tools/bin/xcode_backend.sh: No such file or directory

集成后 Native 工程报错

解决方法:

修改 TARGETS -> Build Setting -> FLUTTER_ROOT 为电脑安装的 Flutter 环境的路径便可。

 

集成后 Native 工程报错

5.如何在 iOS 工程 Debug 模式下使用 release 模式的 flutter

只须要将 Generated.xcconfig 中的 FLUTTER_BUILD_MODE 修改成 release,FLUTTER_FRAMEWORK_DIR 修改成 release 对应的路径便可。

其余

1.说明:

本文仅供用于学习参考,请勿用于商业用途。如需转载,请标明出处,谢谢合做。

本文系参考网络公开 Flutter 学习资料以及我的学习体会总结所得,部份内容为网络公开学习资料,若有侵权请联系做者删除。

2.参考资料:

Flutter 中文网:https://flutterchina.club

咸鱼技术-flutter:https://www.yuque.com/xytech/flutter

iOS Native混编Flutter交互实践:https://juejin.im/post/5bb033515188255c5e66f500#heading-3

Flutter混编之路——开发集成(iOS篇):https://www.jianshu.com/p/48a9083ebe89