美文网首页iOS开发FlutterFlutter
Flutter 开发之 Native 集成 Flutter 混合

Flutter 开发之 Native 集成 Flutter 混合

作者: koin447 | 来源:发表于2019-03-22 15:41 被阅读648次

    本文先介绍一下现有工程如何集成 Flutter 实现混合开发,以及混合项目如何打包,再探索下如何降低原生和 Flutter 之间的依赖,使 Flutter 开发对原生开发的影响尽量降低,以及一些我在尝试中遇到的问题及解决。

    介绍 Flutter

    Flutter 是 Google 发布的一个用于创建跨平台、高性能移动应用的框架。Flutter 和 QT mobile 一样,都没有使用原生控件,相反都实现了一个自绘引擎,使用自身的布局、绘制系统。开发者可以通过 Dart 语言开发 App,一套代码同时运行在 iOS 和 Android平台。Flutter 提供了丰富的组件、接口,开发者可以很快地为 Flutter 添加 Native 扩展。

    前提工作

    开发者需要安装好 Flutter 的环境,执行flutter doctor -v验证。

    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
    
    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。


    bitcode 禁用

    3.为编译 Dart 代码配置 build phase

    打开 iOS 工程,选中项目的 Build Phases 选项,点击左上角+号按钮,选择 New Run Script Phase。


    配置 build 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" 后面。


    配置 build phase

    至此,你可以编译一下工程确保无误:⌘B

    4.在 iOS 工程中使用 FlutterViewController

    首先声明你的 AppDelegate 是 FlutterAppDelegate 的子类。然后定义一个 FlutterEngine 属性,它可以帮助你注册一个没有 FlutterViewController 实例的插件。

    在 AppDelegate.h:

    #import <UIKit/UIKit.h>
    #import <Flutter/Flutter.h>
    
    @interface AppDelegate : FlutterAppDelegate
    @property (nonatomic,strong) FlutterEngine *flutterEngine;
    @end
    

    在AppDelegate.m,修改didFinishLaunchingWithOptions方法如下:

    #import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // 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 <Flutter/Flutter.h>
    #import <UIKit/UIKit.h>
    #import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins
    
    @interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>
    @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<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
    

    在 ViewController 中添加跳转到 FlutterViewController 的测试代码即可:

    #import "ViewController.h"
    #import <Flutter/Flutter.h>
    #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
    
    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 以生成构建版本即可

    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 加入文件夹。

    add_in_project

    加入完成后的结构:

    thirdFramework

    framework 加入后,记住一定要确认 framework 已在 TARGETS -> General -> Embedded Binaries 中添加完成。

    embedded_binaires

    最后改造 APPDelegate 即可:

    #import <UIKit/UIKit.h>
    #import <Flutter/Flutter.h>
    
    @interface AppDelegate : FlutterAppDelegate <UIApplicationDelegate>
    
    @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 <deviceId>' flag, or use '-d all' to act on all devices.

    选择模拟器

    提示你当前有两个模拟器设备,跑设备的时候要选择运行在哪个设备上,flutter run后面拼接上“-d <deviceId>”,deviceId 是第二列的内容。

    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

    build 时可能遇到的错误

    解决方法:

    修改some/flutter_module/.ios/下 Runner 工程的 Bundle Identifier 和原生工程的一致,再次运行flutter build ios即可。

    3.开发时打包产物编译失败

    当你用flutter build ios的产物添加到原生工程中,跳转到 Flutter 界面会黑屏并报出如下错误:

    flutter_build_questions

    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的文件内容:

    flutter_build_ios.png
    flutter_build_ios_debug.png

    可以发现,差别是在于三个文件: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

    集成后 Native 工程报错

    解决方法:

    修改 TARGETS -> Build Setting -> FLUTTER_ROOT 为电脑安装的 Flutter 环境的路径即可。


    集成后 Native 工程报错

    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 客户端开发。

    相关文章

      网友评论

        本文标题:Flutter 开发之 Native 集成 Flutter 混合

        本文链接:https://www.haomeiwen.com/subject/hxhrvqtx.html