本文先介绍一下现有工程如何集成 Flutter 实现混合开发,以及混合项目如何打包,再探索下如何降低原生和 Flutter 之间的依赖,使 Flutter 开发对原生开发的影响尽量降低,以及一些我在尝试中遇到的问题及解决。
介绍 Flutter
Flutter 是 Google 发布的一个用于创建跨平台、高性能移动应用的框架。Flutter 和 QT mobile 一样,都没有使用原生控件,相反都实现了一个自绘引擎,使用自身的布局、绘制系统。开发者可以通过 Dart 语言开发 App,一套代码同时运行在 iOS 和 Android平台。Flutter 提供了丰富的组件、接口,开发者可以很快地为 Flutter 添加 Native 扩展。
前提工作
开发者需要安装好 Flutter 的环境,执行flutter doctor -v
验证。
验证通过后即可开始集成 Flutter。
现有原生工程集成 Flutter
最官方的教程应该是Add Flutter to existing apps了,按照教程如下一步步操作:
1.创建 flutter module
使用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
通过
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。
3.为编译 Dart 代码配置 build phase
打开 iOS 工程,选中项目的 Build Phases 选项,点击左上角+号按钮,选择 New Run Script 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" 后面。
至此,你可以编译一下工程确保无误:⌘B
。
4.在 iOS 工程中使用 FlutterViewController
首先声明你的 AppDelegate 是 FlutterAppDelegate 的子类。然后定义一个 FlutterEngine 属性,它可以帮助你注册一个没有 FlutterViewController 实例的插件。
在 AppDelegate.h:
#import
#import
@interface AppDelegate : FlutterAppDelegate
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end
在AppDelegate.m,修改didFinishLaunchingWithOptions
方法如下:
#import // 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
#import
#import // Only if you have Flutter Plugins
@interface AppDelegate : UIResponder
@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*)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*)delegate {
[_lifeCycleDelegate addDelegate:delegate];
}
@end
在 ViewController 中添加跳转到 FlutterViewController 的测试代码即可:
#import "ViewController.h"
#import
#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
并且你能在控制台中看下如下内容:
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 以生成构建版本即可
混合工程改造优化
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 加入文件夹。
加入完成后的结构:
framework 加入后,记住一定要确认 framework 已在 TARGETS -> General -> Embedded Binaries 中添加完成。
最后改造 APPDelegate 即可:
#import
#import
@interface AppDelegate : FlutterAppDelegate
@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
' flag, or use '-d all' to act on all devices.
提示你当前有两个模拟器设备,跑设备的时候要选择运行在哪个设备上,flutter run
后面拼接上“-d
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
解决方法:
修改some/flutter_module/.ios/
下 Runner 工程的 Bundle Identifier 和原生工程的一致,再次运行flutter build ios
即可。
3.开发时打包产物编译失败
当你用flutter build ios
的产物添加到原生工程中,跳转到 Flutter 界面会黑屏并报出如下错误:
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
的文件内容:
可以发现,差别是在于三个文件: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
解决方法:
修改 TARGETS -> Build Setting -> FLUTTER_ROOT 为电脑安装的 Flutter 环境的路径即可。
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
作者简介
就职于甜橙金融(翼支付)信息技术部,负责 iOS 客户端开发。