运行一个原生的Flutter工程(也就是纯Flutter)非常简便,不过现在Flutter属于试水阶段,要是想在商业app中使用Flutter,目前基本上是将Flutter的页面嵌入到目前先有的iOS或者安卓工程,目前讲混合开发的文章有很多:
不过这些文章大多讲的是安卓和flutter混合开发的,没有iOS和Flutter混合开发的比较详细的步骤实操,上周试了一下iOS和Flutter混合,有一些坑,总结给大家
1.目的
既然用Flutter混合开发,那肯定是希望写一套代码,安卓iOS都能无负担运行,所以在开发的时候,需要满足如下需求:
- Flutter、iOS、安卓工程的目录在同一级,互相之前平级、无嵌套
- 开发iOS的时候,不用操心Flutter部分,只用xcode点击运行就可以(即修改编译iOS项目时,使用编译好的Flutter产物)
- 开发Flutter的时候,不用操心iOS部分,只用android studio点击运行就可以
- 支持模拟器和真机
混合开发最权威的指南当然是flutter自己的wiki,但是缺陷是iOS部分,自动运行脚本的内容不够详细,项目结构也不利于混合开发,本文以其为基础,又对目录结构和脚本做了一些修改,使其便于维护
2.项目搭建
2.1 文件目录搭建
HybridFlutter
|-iOS
|-Android
|-Flutter
|-build
2.2 iOS项目搭建
建立完了上图文件目录,添加iOS工程(安卓工程暂时忽略)
image并且在第一页VC上增加一个Next按钮,集成好Flutter以后,点击Next可以进入Flutter页面
image因为我们要推入flutter页面,所以需要有navigation controller:
image目前Flutter混合开发还不支持bit code,所以在iOS工程里关闭
image2.3 Flutter Module搭建
这里有一个坑,按照flutter官方文档,下载的flutter工具对应其beta分支,是不支持生成Flutter module的,而混合开发的wiki里说,需要建立这么个module,通过咨询大牛,需要切换到master分支,而flutter有个channel命令,可以切换工具分支:
image如果你不在master分支,请执行flutter channel master
之后在Flutter目录下执行flutter create -t module flutter_module
这样就创建好了flutter module
目前为止的目录结构2.4 添加胶水文件
混合开发最关键的是将两个项目衔接起来,所以需要一些配置
2.4.1 xcconfig文件
首先是xcode工程配置的衔接,打开ios工程,在xcode中点击File->New->File添加Configuration Settings File文件,命名为FlutterConfig.xcconfig,
image注意添加的路径是HybridFlutter/Flutter/flutter_module
image此时可能xcode会在ios工程里添加了一个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"
image
然后,将Debug.xcconfig添加到iOS项目的Info-Configuration里:
image2.4.2 AppFrameworkInfo.plist
这个文件在最新的flutter工具里已经自动创建好了
刚才我们看的文件目录,不包含隐藏文件,其实flutter_module里还有对应的ios和android插件工程,都是隐藏文件,从隐藏文件里可以看到AppFrameworkInfo.plist
2.4.3 引入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工程的产物
image把这些产物放到iOS工程里,就能获取到flutter的资源了。
2.4.4 添加flutter编译产物
,将iOS工程目录下的Flutter文件夹添加到工程,然后确保文件夹下的两个framework添加到Embeded Binaries里
image确保flutter_aseets添加到Build Phases里的Copy Bundle Resources里
image添加完,在工程目录里,会多出一个flutter _aseets引用(注意只是引用,如果是拷贝可能会有问题),其实是引用的Flutter/flutter _aseets,试了半天没有去掉,就先这样吧
image目前,所有的胶水文件都已经添加完了,下一步就是在iOS工程里,显示flutter页面
3. 引用Flutter页面
3.1 AppDelegate改造
改变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官方也会更新相关的方法
3.2 推入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页面了:
image因为我们的导航栏使用了iOS原生的,所以flutter的导航栏有点多余了,我们去掉flutter导航栏:
image再次运行:
image证明改动可以同步到app
3.3 flutter页面管理
你可能发现了,上面的代码运行的时候,在flutter页面点击右下角的加号可以增加中间的数字,但是当退出当前页面,再进入flutter页面以后,中间的数字又重置为0了,这是因为每次点击Next,都会重新分配和初始化所有flutter资源,这造成了flutter页面启动慢,状态无法保存(这个页面的数字状态没必要保存,但是别的场景下一定有需要保存的内容)
所以Flutter新锐专家之路:混合开发篇对混合开发中flutter部分做了很好的管理,它将flutter部分做成单例,使其基础资源在app运行期间只运行一次,再将flutter根页面设置成一个空白container,需要flutter推入什么页面,就发消息给flutter,flutter在空白container基础上推入对应页面,这样当从flutter的某个页面回退到iOS原生页面的时候,flutter也会释放掉刚刚显示的页面,回退到空白页面。
4. 配置自动运行脚本
针对怎么写代码,不是这篇文章的范畴,下面说说混合开发最后的一个痛点
现在的工程,flutter部分有改动,可以直接通过绑定的xcode-backend.sh
来编译,并生成framework和资源文件,所以无论是iOS端,还是flutter端有改动,在xcode上点击run都可以运行到模拟器和真机,而且iOS和flutter项目代码彼此独立,只有flutter的编译产物留在了iOS文件夹里
但是现在还有一个问题,就是当开发flutter部分的时候,我们并不想碰xcode,最好能关掉xcode,只打开android studio做开发,然后点击AS上的run按钮运行。
4.1 实现原理
- xcode命令行工具,可以编译iOS项目(就像xcode里点击run一样),并且还能指定生成.app文件的目录
- flutter运行的时候,可以指定--use-application-binary,flutter编译产物,以hot-load的方式注入到指定app中(这个原理是我自己猜的,实际情况待仔细确认)
通过上述两步,就可以在android studio里,直接往iOS系统里安装混合app了
4.2 模拟器实现
用android studio打开flutter_module文件夹
image可以看到右上角已经是可以run的状态了,但是点击的话,会有如下错误提示:
image原因很简单,这个flutter_module不是一个独立的工程,需要依赖一个app,所以我们需要先编译出iOS app,并放到好找的位置:
点击下图的Edit Configurations
image
然后添加一个运行前编译app的命令,点击下图的Run External tool
image
添加下面的一条:
imageProgram里填/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
,这里面指定了编译的参数
添加后如图:
image接着添加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
4.3 真机
真机是一样的原理,就是命令参数不一样:
运行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运行
5 总结
flutter混合开发,需要手动设置的地方很多,但是一旦设置好,就不需要再改动,至于最后的flutter运行参数,需要指定绝对路径,不知道什么原因,好在影响不大,有空再仔细研究。希望本文会对你有帮助
网友评论