概述
本文不想写一个全篇步骤式的文章来描写怎么集成 flutter,而是期望用一种探索的方式来追寻答案。
原理分析
我们首先看下 flutter 项目和一般原生项目的大概区别。
为了跳转方便,原生项目的入口一般是 UINavigationController。
而我们看下 flutter 默认给我们创建的模板为:
这里我们来看下 flutter 的引擎源码,看下这段代码做了什么工作,源码路径为:https://github.com/flutter/en…
我们首先看下 `FlutterAppDelegate
https://github.com/flutter/en…
– (instancetype)init {
if (self = [super init]) {
_lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
}
return self;
}
….
– (BOOL)application:(UIApplication*)application
willFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
return [_lifeCycleDelegate application:application willFinishLaunchingWithOptions:launchOptions];
}
….
所以这里可以看到,FlutterAppDelegate 完全是调用了 FlutterPluginAppLifeCycleDelegate 的所有方法。假设你的项目原先就有一个 AppDelegate 的实现类,那么可以参考 FlutterAppDelegate 的源码,创建一个 FlutterPluginAppLifeCycleDelegate, 并在所有方法中调用这个类实例的方法。
原生项目中创建根 ViewControler 的方式可以使用 StoryBoard,也可以使用代码创建。而 flutter 模板给我们创建的项目为 StoryBoard 的方式
从这里我们可以发现,flutter 默认项目模板是将 FlutterViewController 作为根 ViewController。
项目实战
创建项目
原理分析完毕,我们可以创建一个工程项目了.
我们这里选择创建一个最常见的 SingleViewApp
改成不使用 StoryBoard,而是代码创建根 ViewController
为了演示方便,我们创建一个 controller
修改一下启动代码:
– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
UIViewController* main = [[MainViewController alloc]initWithNibName:@”MainViewController” bundle:nil];
UINavigationController* root = [[UINavigationController alloc]initWithRootViewController:main];
self.window.backgroundColor = [UIColor whiteColor];
self.window.rootViewController = root;
[self.window makeKeyAndVisible];
return YES;
}
在 MainViewController 中,我们摆上两个按钮:
创建 flutter 模块
我们使用 flutter 自带命令创建一个 flutter 模块项目
flutter create -t module my_flutter
把创建出来的所有文件一起拷贝到上面 ios 原生项目的同一级目录中:
使用 pod 初始化一下项目:
cd myproject
pod init
这样就生成了 Podfile
我们打开修改一下,以便将 flutter 包括在里面
platform :ios, ‘9.0’
target ‘myproject’ do
end
# 新添加的代码
flutter_application_path = ‘../’
eval(File.read(File.join(flutter_application_path, ‘.ios’, ‘Flutter’, ‘podhelper.rb’)), binding)
运行下 pod 安装
pod install
我们可以看到,与刚才相比,新增加了 workspace 文件,我们关掉原来的项目,并打开 workspace
然后我们可以看到项目结构如下:
编译一下:
ld: ‘/Users/jzoom/SourceCode/myproject/myproject/DerivedData/myproject/Build/Products/Debug-iphoneos/FlutterPluginRegistrant/libFlutterPluginRegistrant.a(GeneratedPluginRegistrant.o)’ does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. file ‘/Users/jzoom/SourceCode/myproject/myproject/DerivedData/myproject/Build/Products/Debug-iphoneos/FlutterPluginRegistrant/libFlutterPluginRegistrant.a’ for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
出现了这个错误
打开项目编译配置,并搜索 bit, 出现下面结果:
修改下 Enable Bitcode 为 No
此时编译 ok。
至此,在原生项目中配置 flutter 完毕,我们开始开发功能。
修改 AppDelegate
由于我们的 AppDelegate 不是 FlutterAppDelegate,所以我们按照前面分析的路子,改成如下:
//
// AppDelegate.m
// myproject
//
// Created by JZoom on 2019/4/9.
// Copyright © 2019 JZoom. All rights reserved.
//
#import “AppDelegate.h”
#import “GeneratedPluginRegistrant.h”
#import <Flutter/Flutter.h>
#import “MainViewController.h”
@interface AppDelegate()<FlutterPluginRegistry>
@end
@implementation AppDelegate{
FlutterPluginAppLifeCycleDelegate* _lifeCycleDelegate;
}
– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
UIViewController* main = [[MainViewController alloc]initWithNibName:@”MainViewController” bundle:nil];
UINavigationController* root = [[UINavigationController alloc]initWithRootViewController:main];
self.window.backgroundColor = [UIColor whiteColor];
self.window.rootViewController = root;
[self.window makeKeyAndVisible];
[GeneratedPluginRegistrant registerWithRegistry:self];
return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}
– (instancetype)init {
if (self = [super init]) {
_lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
}
return self;
}
– (void)dealloc {
_lifeCycleDelegate = nil;
}
– (BOOL)application:(UIApplication*)application
willFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
return [_lifeCycleDelegate application:application willFinishLaunchingWithOptions: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];
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored “-Wdeprecated-declarations”
– (void)application:(UIApplication*)application
didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings {
[_lifeCycleDelegate application:application
didRegisterUserNotificationSettings:notificationSettings];
}
#pragma GCC diagnostic pop
– (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];
}
– (void)application:(UIApplication*)application
didReceiveLocalNotification:(UILocalNotification*)notification {
[_lifeCycleDelegate application:application didReceiveLocalNotification:notification];
}
– (void)userNotificationCenter:(UNUserNotificationCenter*)center
willPresentNotification:(UNNotification*)notification
withCompletionHandler:
(void (^)(UNNotificationPresentationOptions options))completionHandler
API_AVAILABLE(ios(10)) {
if (@available(iOS 10.0, *)) {
[_lifeCycleDelegate userNotificationCenter:center
willPresentNotification:notification
withCompletionHandler: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 (^)())completionHandler {
[_lifeCycleDelegate application:application
handleEventsForBackgroundURLSession:identifier
completionHandler:completionHandler];
}
– (void)application:(UIApplication*)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
[_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];
}
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 120000
– (BOOL)application:(UIApplication*)application
continueUserActivity:(NSUserActivity*)userActivity
restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>>* __nullable
restorableObjects))restorationHandler {
#else
– (BOOL)application:(UIApplication*)application
continueUserActivity:(NSUserActivity*)userActivity
restorationHandler:(void (^)(NSArray* __nullable restorableObjects))restorationHandler {
#endif
return [_lifeCycleDelegate application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
#pragma mark – FlutterPluginRegistry methods. All delegating to the rootViewController
– (NSObject<FlutterPluginRegistrar>*)registrarForPlugin:(NSString*)pluginKey {
UIViewController* rootViewController = _window.rootViewController;
if ([rootViewController isKindOfClass:[FlutterViewController class]]) {
return
[[(FlutterViewController*)rootViewController pluginRegistry] registrarForPlugin:pluginKey];
}
return nil;
}
– (BOOL)hasPlugin:(NSString*)pluginKey {
UIViewController* rootViewController = _window.rootViewController;
if ([rootViewController isKindOfClass:[FlutterViewController class]]) {
return [[(FlutterViewController*)rootViewController pluginRegistry] hasPlugin:pluginKey];
}
return false;
}
– (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
UIViewController* rootViewController = _window.rootViewController;
if ([rootViewController isKindOfClass:[FlutterViewController class]]) {
return [[(FlutterViewController*)rootViewController pluginRegistry]
valuePublishedByPlugin:pluginKey];
}
return nil;
}
#pragma mark – FlutterAppLifeCycleProvider methods
– (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate {
[_lifeCycleDelegate addDelegate:delegate];
}
@end
创建 FlutterViewController
编辑下 MainViewController
– (IBAction)launchFlutter1:(id)sender {
FlutterViewController* c = [[FlutterViewController alloc]init];
[self.navigationController pushViewController:c animated:YES];
}
编译下, 运行点击按钮调取 flutter 视图,发现一片空白,并出现如下错误:
2019-04-09 13:18:18.500285+0800 myproject[57815:1968395] [VERBOSE-1:callback_cache.cc(132)] Could not parse callback cache, aborting restore
2019-04-09 13:18:36.554643+0800 myproject[57815:1968395] Failed to find assets path for “Frameworks/App.framework/flutter_assets”
2019-04-09 13:18:36.658247+0800 myproject[57815:1969776] [VERBOSE-2:engine.cc(116)] Engine run configuration was invalid.
2019-04-09 13:18:36.659545+0800 myproject[57815:1969776] [VERBOSE-2:FlutterEngine.mm(294)] Could not launch engine with configuration.
2019-04-09 13:18:36.816199+0800 myproject[57815:1969793] flutter: Observatory listening on http://127.0.0.1:50167/
我们看看和 flutter 自己创建的项目比,还差了什么
如图:有三个地方, 我们把这些文件 copy 一份放到我们的项目中, 并且设置一下编译选项:
修改下项目的配置,增加一个脚本
/bin/sh “$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh” thin
放到 Copy Bundle Resources 下面
结果:
进行优化
在上面的步骤里面,我们通过直接文件拷贝将.ios 目录下的 flutter 生成文件拷贝到了原生项目里面,显然我们不能每一次都手动这么做,我们可以添加一个命令来做这件事。
rm -rf ${SOURCE_ROOT}/Flutter/Generated.xcconfig
cp -ri ../.ios/Flutter/Generated.xcconfig ${SOURCE_ROOT}/Flutter/Generated.xcconfig
rm -rf ${SOURCE_ROOT}/Flutter/App.framework
cp -ri ../.ios/Flutter/App.framework ${SOURCE_ROOT}/Flutter/App.framework
我们把这个命令放到前面去
问题
Q : 如何调用 flutter 的不同页面?
A : 我们首先定义一下路由
然后我们可以这么调用
/// flutter 的路由视图
FlutterViewController* c = [[FlutterViewController alloc]init];
;
[self.navigationController pushViewController:c animated:YES];
Q : 如何在原生项目中调试 flutter?A : 首先在命令行启动 flutter 的监听
flutter attach
如果有多台设备,需要选择一下设备
flutter attach -d 设备标志
然后就可以在 xcode 中启动调试运行项目
改动代码之后按下键盘上面的 r 键就可以了。