iOS Flutter 与 原生混合开发

1.系统要求

  • 安装了 Flutter(Flutter 版本 1.2 及更高版本)
  • 安装了 Xcode(支持 iOS8 及更高版本,安卓 Java8, Android Studio 3.6才支持Android Studio的应用程序添加流程)

2.创建 Flutter module

  1. 终端进入某个路径,创建 Flutter module
cd some/path/
flutter create --template module {moduleName}

运行结果:


创建 module 结果.png

这样就创建了一个 名为 my_flutterFlutter module 项目

my_flutter 目录结构

3.创建一个 iOS 工程

创建一个名为 MyAppiOS 工程项目,创建的路径最好跟创建的Flutter module在同一个路径下面

4.在现有应用程序中嵌入 Flutter module

有两种方法嵌入module到现有的应用程序中

  1. 使用 CocoaPods 依赖管理器和已安装的 Flutter SDK(官网推荐使用该方式,在这里也只介绍这一种方式)
  2. 通过手动编译 Flutter enginedart代码和所有 Flutter pluginframework,用 Xcode 手动集成到你的应用中,并更新编译设置(官网介绍)

4.1 使用 CocoaPods 方式

  1. 给现有的 MyApp 创建一个 Podfile,并且在里面加入以下代码

    
    platform :ios,'10.0'
    inhibit_all_warnings!
    
    # flutter module 文件路径
    flutter_application_path = '../my_flutter' 
    load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
    
    
    target 'MyApp' do
    
    install_all_flutter_pods(flutter_application_path)
    
    end
    
  2. 执行 pod install

执行 pod install 结果.png

5. iOS 与 Flutter 的通信(界面跳转)

为了在既有的iOS应用中展示Flutter页面,需要启动 Flutter EngineFlutterViewController,通常建议为我们的应用预热一个长时间存活的 FlutterEngine,我们将在应用启动的 appdelegate 中创建一个 FlutterEngine,并作为属性暴露给外界.

AppDelegate.h

@import UIKit;
@import Flutter;

@interface AppDelegate : FlutterAppDelegate

@property (nonatomic, strong) FlutterEngine *flutterEngine;

@end

AppDelegate.m

#import "AppDelegate.h"
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h>

@interface AppDelegate ()

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey,id> *)launchOptions {
    
    self.flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"];
    [self.flutterEngine run];
    [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
    
    return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

下面的示例显示一个通用的 ViewController,其中有一个 UIButton 用于显示 FlutterViewControllerFlutterViewController 使用在 AppDelegate 中创建的 FlutterEngine 实例。

ViewController.m

#import "ViewController.h"
#import "AppDelegate.h"
@import Flutter;

@interface ViewController ()

@property (nonatomic, strong) FlutterMethodChannel *messageChannel;
@property (nonatomic, strong) UILabel *resultLabel;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIButton *button = [UIButton new];
    button.backgroundColor = UIColor.blueColor;
    button.frame = CGRectMake(80, 210, 160, 40);
    [button addTarget:self
               action:@selector(buttonClicked)
     forControlEvents:UIControlEventTouchUpInside];
    [button setTitle:@"OC 调用 Flutter" forState:UIControlStateNormal];
    [self.view addSubview:button];

    self.resultLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 250, self.view.bounds.size.width, 50)];
    self.resultLabel.font = [UIFont systemFontOfSize:14.0f];
    self.resultLabel.textAlignment = NSTextAlignmentCenter;
    [self.view addSubview:self.resultLabel];
}

- (void)buttonClicked {
    
    // 获取全局的 flutterEngine,防止跳转的时候卡顿
    FlutterEngine *flutterEngine = ((AppDelegate *)UIApplication.sharedApplication.delegate).flutterEngine;
    // 跳转的界面控制器
    FlutterViewController *flutterController = [[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil];
    // 设置标记,需要跟 dart 中的 main.dart 一致
    NSString *channelName = @"com.pages.your/native_get";
    // 创建 FlutterMethodChannel
    _messageChannel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:flutterController.binaryMessenger];
    // 设置当前 FlutterMethodChannel 的方法名和需要传递过去的参数
    [_messageChannel invokeMethod:@"NativeToFlutter" arguments:@[@"原生调用Flutter参数1", @"原生调用Flutter参数2"]];
    // 跳转界面
    [self.navigationController pushViewController:flutterController animated:YES];
    // Flutter 回调
    __weak typeof(self) weakSelf = self;
    [_messageChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
        [flutterController.navigationController popViewControllerAnimated:YES];
        NSArray *array = (NSArray *)call.arguments;
        NSMutableString *mutableString = [NSMutableString string];
        for (NSString *string in array) {
            [mutableString appendFormat:@"%@ ", string];
        }
        weakSelf.resultLabel.text = mutableString;
    }];
}

Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: _HomePage()
    );
  }
}

class _HomePage extends StatefulWidget {
  @override
  __HomePageState createState() => __HomePageState();
}

class __HomePageState extends State<_HomePage> {

  String title = "Flutter to Native";
  Color backgroundColor = Colors.red;
  // 创建 MethodChannel 这里的标志跟 ios 中设置要一致
  static const MethodChannel methodChannel = const MethodChannel('com.pages.your/native_get');
  // Flutter 调用原生
  _iOSPushToVC() {
    methodChannel.invokeMethod('FlutterToNative', ["Flutter 调用原生参数 1", "Flutter 调用原生参数 2"]);
  }

  @override
  void initState() {
    super.initState();
    // 设置原生调用 Flutter 回调,获取到方法名和参数
    methodChannel.setMethodCallHandler((MethodCall call){
      if (call.method == "NativeToFlutter") {
        setState(() {
          List<dynamic> arguments = call.arguments;
          String str = "";
          for (dynamic string in arguments) {
            str = str + " " + string;
          }
          title = str;
          backgroundColor = Colors.orange;
        });
      }
      return Future<dynamic>.value();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: backgroundColor,
      body: Center(
        child: GestureDetector(
          behavior: HitTestBehavior.opaque,
          child: Text(title),
          onTap: (){
            _iOSPushToVC();
          },
        ),
      ),
    );
  }
}

运行结果为:

iOS与Flutter通信.gif

这样一个原生与 Flutter 的通信就完成了,原生调用 Flutter 界面并且传递参数过去,Flutter 回到原生带回参数.

6. 热加载

  1. 关闭 App

  2. terminal 中运行 flutter attach 命令。(这里需要进入到 Flutter module 工程路径,不然会报找不到 lib/main.dart 错误)

    2.1 如果当前有多台设备,会出现如下提示

    多台设备提示

    2.2 使用以下命令

    flutter attach -d {设备标识}
    

    2.3 启动 App

    启动 App 之后,终端显示.png

2.4 这样就可以在终端使用热加载了

```
r : 热加载;
R : 热重启;
h : 获取帮助;
d : 断开连接;
q : 退出
```

7. 使用 Flutter_Boost 进行交互

Flutter_Boost 是阿里巴巴-闲鱼技术提供的Flutter-Native混合解决方案.FlutterBoost是一个Flutter插件,它可以轻松地为现有原生应用程序提供Flutter混合集成方案。FlutterBoost的理念是将FlutterWebview那样来使用。在现有应用程序中同时管理Native页面和Flutter页面并非易事。 FlutterBoost帮你处理页面的映射和跳转,你只需关心页面的名字和参数即可(通常可以是URL)。

7.1 前置条件

在继续之前,您需要将Flutter集成到你现有的项目中。flutter sdk 的版本需要和boost版本适配,否则会编译失败.


boost 版本说明.png

7.2 安装

7.2.1 添加到 pubspec.yaml 文件中
dependencies:
  flutter_boost: ^1.12.13+3
7.2.2 初始化
flutter pub get
7.2.3 Dart 代码
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_boost/flutter_boost.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    // 注册路由
    FlutterBoost.singleton.registerPageBuilders(<String, PageBuilder>{
      'first':
          (String pageName, Map<String, dynamic> params, String uniqueId) =>
              FirstRouteWidget(),
    });
  }

  void _onRoutePushed(
    String pageName,
    String uniqueId,
    Map<String, dynamic> params,
    Route<dynamic> route,
    Future<dynamic> _,
  ) {}

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Boost example',
        // 初始化
        builder: FlutterBoost.init(postPush: _onRoutePushed),
        home: Container(color: Colors.white));
  }
}

/// 需要跳转的界面
class FirstRouteWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(title: 'First', home: Container(color: Colors.orange));
  }
}
7.2.4 native代码
  1. 新建 PlatformRouterImp 文件,作为路由管理.继承于 NSObject,并且遵守 FLBPlatform 协议

    PlatformRouterImp.h

    #import <Foundation/Foundation.h>
    #import <UIKit/UIKit.h>
    #import <FlutterBoost.h>
    
    @interface PlatformRouterImp : NSObject<FLBPlatform>
    
    #pragma mark - Property
    
    @property (nonatomic, weak) UINavigationController *navigationController;
    
    
    #pragma mark - Method
    
    + (instancetype)shareRouter;
    
    @end
    

    PlatformRouterImp.m

    #import "PlatformRouterImp.h"
    
    static PlatformRouterImp *_router;
    
    @interface PlatformRouterImp ()
    
    
    
    @end
    
    @implementation PlatformRouterImp
    
    #pragma mark - Lifecycle
    
    + (instancetype)shareRouter {
        
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            _router = [[PlatformRouterImp alloc] init];
        });
        
        return _router;
    }
    
    - (void)dealloc {
        
        NSLog(@"%s", __func__);
    }
    
    
    #pragma mark - Custom Accessors (Setter 方法)
    
    
    #pragma mark - Public
    
    
    #pragma mark - Private
    
    
    #pragma mark - Protocol
    
    /**
    * 基于Native平台实现页面打开,Dart层的页面打开能力依赖于这个函数实现;Native或者Dart侧不建议直接使用这个函数。应直接使用FlutterBoost封装的函数
    *
    * @param url 打开的页面资源定位符
    * @param urlParams 传人页面的参数; 若有特殊逻辑,可以通过这个参数设置回调的id
    * @param exts 额外参数
    * @param completion 打开页面的即时回调,页面一旦打开即回调
    */
    - (void)open:(NSString *)url
      urlParams:(NSDictionary *)urlParams
            exts:(NSDictionary *)exts
      completion:(void (^)(BOOL finished))completion {
        
        FLBFlutterViewContainer *controller = [FLBFlutterViewContainer new];
        // 这句代码千万不能省略
        [controller setName:url params:urlParams];
        if (self.navigationController) {
            [self.navigationController pushViewController:controller animated:YES];
        }
        if (completion) {
            completion(YES);
        }
    }
    
    
    #pragma mark - 懒加载
    
    
    
    @end
    
  2. AppDelegate.m

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        
        PlatformRouterImp *router = [PlatformRouterImp shareRouter];
        [FlutterBoostPlugin.sharedInstance startFlutterWithPlatform:router onStart:^(FlutterEngine * _Nonnull engine) {
            
        }];
        
        return YES;
    }
    
  3. ViewController.m

    #import "ViewController.h"
    #import <FlutterBoost.h>
    #import "PlatformRouterImp.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(100, 230, 250, 30)];
        button.backgroundColor = UIColor.orangeColor;
        [button setTitle:@"跳转 FLutter" forState:UIControlStateNormal];
        [button addTarget:self action:@selector(buttonClicked) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:button];
    }
    
    - (void)buttonClicked {
        
        NSLog(@"跳转 Flutter");
        PlatformRouterImp.shareRouter.navigationController = self.navigationController;
        [FlutterBoostPlugin open:@"first" urlParams:@{kPageCallBackId:@"MycallbackId#1"} exts:@{@"animated":@(YES)} onPageFinished:^(NSDictionary *result) {
            NSLog(@"call me when page finished, and your result is:%@", result);
        } completion:^(BOOL f) {
            NSLog(@"page is opened");
        }];
    }
    

    这样就是用了 Flutter_Boost 进行原生与 Flutter 的跳转.

    Flutter_Boost_Native_Flutter.gif

[controller setName:url params:urlParams]不能省略,省略了会报错

省略了报错

8.断点调试

  1. 先将 App 进行杀掉

  2. Android Studio 中进行操作

    断点调试操作.png

  3. 打开 Xcode 进行 run 工程

  4. 这样就可以进行断点调试了.

  • 有时候连接设备按钮为灰色的,需要先打开模拟器或者连接真机设备,然后打开 Android Studio进行连接设备

  • 每次修改了 pubspec.yaml文件并且 flutter pub get 之后,需要在 iOS 工程执行 pod install 操作

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345