运行一个原生的Flutter工程(也就是纯Flutter)很是简便,不过如今Flutter属于试水阶段,要是想在商业app中使用Flutter,目前基本上是将Flutter的页面嵌入到目前先有的iOS或者安卓工程,目前讲混合开发的文章有不少:android
Flutter混合工程开发探究github
不过这些文章大多讲的是安卓和flutter混合开发的,没有iOS和Flutter混合开发的比较详细的步骤实操,上周试了一下iOS和Flutter混合,有一些坑,总结给你们bash
既然用Flutter混合开发,那确定是但愿写一套代码,安卓iOS都能无负担运行,因此在开发的时候,须要知足以下需求:app
混合开发最权威的指南固然是flutter本身的wiki,可是缺陷是iOS部分,自动运行脚本的内容不够详细,项目结构也不利于混合开发,本文以其为基础,又对目录结构和脚本作了一些修改,使其便于维护iphone
HybridFlutter
|-iOS
|-Android
|-Flutter
|-build
复制代码
创建完了上图文件目录,添加iOS工程(安卓工程暂时忽略)ide
而且在第一页VC上增长一个Next按钮,集成好Flutter之后,点击Next能够进入Flutter页面工具
由于咱们要推入flutter页面,因此须要有navigation controller:
目前Flutter混合开发还不支持bit code,因此在iOS工程里关闭
这里有一个坑,按照flutter官方文档,下载的flutter工具对应其beta分支,是不支持生成Flutter module的,而混合开发的wiki里说,须要创建这么个module,经过咨询大牛,须要切换到master分支,而flutter有个channel命令,能够切换工具分支:
若是你不在master分支,请执行flutter channel master
以后在Flutter目录下执行flutter create -t module flutter_module
这样就建立好了flutter module
混合开发最关键的是将两个项目衔接起来,因此须要一些配置
首先是xcode工程配置的衔接,打开ios工程,在xcode中点击File->New->File添加Configuration Settings File文件,命名为FlutterConfig.xcconfig,
在FlutterConfig.xcconfig里添加 #include "./.ios/Flutter/Generated.xcconfig"
引用flutter_module下的ios插件里的Generated.xcconfig文件
上面是给flutter添加xcconfig文件,下载添加ios工程里的xccofig文件Debug.xcconfig
,并引用FlutterConfig.xcconfig(若是iOS工程里已经有了xcconfig文件,那么直接在已有的xcconfig里添加)
#include "../../../Flutter/flutter_module/FlutterConfig.xcconfig"
而后,将Debug.xcconfig添加到iOS项目的Info-Configuration里:
这个文件在最新的flutter工具里已经自动建立好了 刚才咱们看的文件目录,不包含隐藏文件,其实flutter_module里还有对应的ios和android插件工程,都是隐藏文件,从隐藏文件里能够看到AppFrameworkInfo.plist
xcode-backend.sh
在ios工程里添加运行脚本"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
,而且确保Run Script这一行在 "Target dependencies" 或者 "Check Pods Manifest.lock"后面
此时点击xcode的运行,会执行到xcode-backend.sh脚本,因此不只会编译安装iOS app到模拟器(暂时运行对象是模拟器),并且在iOS工程目录,也会生成一个Flutter文件夹,里面是Flutter工程的产物
把这些产物放到iOS工程里,就能获取到flutter的资源了。
,将iOS工程目录下的Flutter文件夹添加到工程,而后确保文件夹下的两个framework添加到Embeded Binaries里
添加完,在工程目录里,会多出一个flutter _aseets引用(注意只是引用,若是是拷贝可能会有问题),实际上是引用的Flutter/flutter _aseets,试了半天没有去掉,就先这样吧
目前,全部的胶水文件都已经添加完了,下一步就是在iOS工程里,显示flutter页面
改变AppDelegate.h,使其父类指向FlutterAppDelegate:
#import <Flutter/Flutter.h>
@interface AppDelegate : FlutterAppDelegate <UIApplicationDelegate, FlutterAppLifeCycleProvider>
@end
复制代码
改造AppDelegate.m
//
// AppDelegate.m
// HybridIOS
//
// Created by Realank on 2018/8/20.
// Copyright © 2018年 Realank. All rights reserved.
//
#import "AppDelegate.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
{
FlutterPluginAppLifeCycleDelegate *_lifeCycleDelegate;
}
- (instancetype)init {
if (self = [super init]) {
_lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
}
return self;
}
- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
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
复制代码
这部分改造的原理尚未深究,并且有一些方法的实现iOS已经提示弃用了,你们在加入已有工程的时候,须要酌情考虑,我相信后续flutter官方也会更新相关的方法
在首页VC中添加以下代码
//
// ViewController.m
// HybridIOS
//
// Created by Realank on 2018/8/20.
// Copyright © 2018年 Realank. All rights reserved.
//
#import "ViewController.h"
#import <Flutter/Flutter.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
- (IBAction)goNext:(id)sender {
FlutterViewController* flutterViewController = [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
FlutterBasicMessageChannel* messageChannel = [FlutterBasicMessageChannel messageChannelWithName:@"channel"
binaryMessenger:flutterViewController
codec:[FlutterStandardMessageCodec sharedInstance]];//消息发送代码,本文不作解释
__weak __typeof(self) weakSelf = self;
[messageChannel setMessageHandler:^(id message, FlutterReply reply) {
// Any message on this channel pops the Flutter view.
[[weakSelf navigationController] popViewControllerAnimated:YES];
reply(@"");
}];
NSAssert([self navigationController], @"Must have a NaviationController");
[[self navigationController] pushViewController:flutterViewController animated:YES];
}
@end
复制代码
若是你的首页不在navigation controller里,那么pushflutter页面确定会报错,这和flutter不要紧,若是确实没有navigation controller,能够present flutterViewController
运行代码,点击next,就能够看到flutter页面了:
由于咱们的导航栏使用了iOS原生的,因此flutter的导航栏有点多余了,咱们去掉flutter导航栏:
证实改动能够同步到app
你可能发现了,上面的代码运行的时候,在flutter页面点击右下角的加号能够增长中间的数字,可是当退出当前页面,再进入flutter页面之后,中间的数字又重置为0了,这是由于每次点击Next,都会从新分配和初始化全部flutter资源,这形成了flutter页面启动慢,状态没法保存(这个页面的数字状态不必保存,可是别的场景下必定有须要保存的内容)
因此Flutter新锐专家之路:混合开发篇对混合开发中flutter部分作了很好的管理,它将flutter部分作成单例,使其基础资源在app运行期间只运行一次,再将flutter根页面设置成一个空白container,须要flutter推入什么页面,就发消息给flutter,flutter在空白container基础上推入对应页面,这样当从flutter的某个页面回退到iOS原生页面的时候,flutter也会释放掉刚刚显示的页面,回退到空白页面。
针对怎么写代码,不是这篇文章的范畴,下面说说混合开发最后的一个痛点
如今的工程,flutter部分有改动,能够直接经过绑定的xcode-backend.sh
来编译,并生成framework和资源文件,因此不管是iOS端,仍是flutter端有改动,在xcode上点击run均可以运行到模拟器和真机,并且iOS和flutter项目代码彼此独立,只有flutter的编译产物留在了iOS文件夹里 可是如今还有一个问题,就是当开发flutter部分的时候,咱们并不想碰xcode,最好能关掉xcode,只打开android studio作开发,而后点击AS上的run按钮运行。
经过上述两步,就能够在android studio里,直接往iOS系统里安装混合app了
用android studio打开flutter_module文件夹
能够看到右上角已是能够run的状态了,可是点击的话,会有以下错误提示:
缘由很简单,这个flutter_module不是一个独立的工程,须要依赖一个app,因此咱们须要先编译出iOS app,并放到好找的位置:
点击下图的Edit Configurations
而后添加一个运行前编译app的命令,点击下图的Run External tool
Program里填/usr/bin/env
,Arguments里填xcrun xcodebuild build -configuration Debug VERBOSE_SCRIPT_LOGGING=YES -project ../../iOS/HybridIOS/HybridIOS.xcodeproj -scheme HybridIOS BUILD_DIR=../build/ios -sdk iphonesimulator -arch x86_64
,这里面指定了编译的参数
添加后如图:
接着添加flutter编译的参数,指定刚刚编译出来的app做为hotload的宿主app: --use-application-binary /Users/realank/Documents/GitHub/HybridFlutter/iOS/build/ios/Debug-iphonesimulator/HybridIOS.app
这里须要注意,我一开始使用相对路径,怎么也运行不起来,说找不到对应的app,因此我使用了绝对路径,你要换成本身的HybridFlutter/iOS/build/ios/Debug-iphonesimulator/HybridIOS.app
的绝对路径
大功告成,这时候点击run运行,就会先编译ipa,在运行flutter
真机是同样的原理,就是命令参数不同:
运行flutter前编译app的命令:xcrun xcodebuild build -configuration Debug VERBOSE_SCRIPT_LOGGING=YES -project ../../iOS/HybridIOS/HybridIOS.xcodeproj -scheme HybridIOS BUILD_DIR=../build/ios -sdk iphoneos -arch arm64
真机的app和模拟机app的产物路径不同,因此flutter参数也得变: --use-application-binary /Users/realank/Documents/GitHub/HybridFlutter/iOS/build/ios/Debug-iphoneos/HybridIOS.app
这样,咱们就能够选择想要运行的是真机仍是模拟器,而后点击run运行
flutter混合开发,须要手动设置的地方不少,可是一旦设置好,就不须要再改动,至于最后的flutter运行参数,须要指定绝对路径,不知道什么缘由,好在影响不大,有空再仔细研究。但愿本文会对你有帮助