7

概述

本文不想写一个全篇步骤式的文章来描写怎么集成flutter,而是期望用一种探索的方式来追寻答案。

原理分析

我们首先看下flutter项目和一般原生项目的大概区别。

为了跳转方便,原生项目的入口一般是UINavigationController

而我们看下flutter默认给我们创建的模板为:

clipboard.png

clipboard.png

这里我们来看下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的方式

clipboard.png

clipboard.png

从这里我们可以发现,flutter默认项目模板是将FlutterViewController作为根ViewController。

项目实战

创建项目

原理分析完毕,我们可以创建一个工程项目了.

我们这里选择创建一个最常见的SingleViewApp

clipboard.png

clipboard.png

改成不使用StoryBoard,而是代码创建根ViewController

clipboard.png

为了演示方便,我们创建一个controller
clipboard.png

修改一下启动代码:


- (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中,我们摆上两个按钮:

clipboard.png

创建flutter模块

我们使用flutter自带命令创建一个flutter模块项目

flutter create -t module my_flutter

把创建出来的所有文件一起拷贝到上面ios原生项目的同一级目录中:

clipboard.png

使用pod初始化一下项目:

cd myproject
pod init

这样就生成了Podfile

clipboard.png

我们打开修改一下,以便将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

clipboard.png

我们可以看到,与刚才相比,新增加了workspace文件,我们关掉原来的项目,并打开workspace

clipboard.png

然后我们可以看到项目结构如下:

clipboard.png

编译一下:

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,出现下面结果:

clipboard.png

修改下Enable Bitcode为No

clipboard.png

此时编译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自己创建的项目比,还差了什么

clipboard.png

如图:有三个地方,我们把这些文件copy一份放到我们的项目中,并且设置一下编译选项:

clipboard.png

修改下项目的配置,增加一个脚本

clipboard.png

/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" thin

clipboard.png

放到Copy Bundle Resources下面

结果:

clipboard.png

进行优化

在上面的步骤里面,我们通过直接文件拷贝将.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

我们把这个命令放到前面去
clipboard.png

问题

Q : 如何调用flutter的不同页面?

A : 我们首先定义一下路由

clipboard.png

然后我们可以这么调用

 /// flutter的路由视图
    FlutterViewController* c = [[FlutterViewController alloc]init];
    [c setInitialRoute:@"page2"];
    [self.navigationController pushViewController:c animated:YES];

Q : 如何在原生项目中调试flutter?
A : 首先在命令行启动flutter的监听

flutter attach

如果有多台设备,需要选择一下设备

flutter attach -d 设备标志

然后就可以在xcode中启动调试运行项目

clipboard.png

改动代码之后按下键盘上面的r键就可以了。


jzoom
1.2k 声望334 粉丝

A simple way to solving problems is using tools like docker/Spring boot/React Native/React/Vue… Technology should not become a bottleneck in thinking.