将Flutter Module集成到iOS工程中

1. Flutter环境搭建

1.1. 环境

  • 时间:2022.7.13
  • 电脑:MacBook Pro (13-inch, M1, 2020)
  • 系统:macOS Monterey 12.0.1 (21A559)
  • Xcode版本:13.4.1 (13F100)
  • Android Studio版本:Chipmunk | 2021.2.1 Patch 1
    • Flutter插件版本:69.0.2
    • Dart插件版本:212.5744
  • Flutter SDK版本:3.0.4
  • CocoaPods版本:1.11.3
  • flutter_boost官方文档
  • 默认已安装HomebrewCocoaPods,如果未安装可以参考:

1.2. 下载Flutter库文件

  • 下载链接
    下载最新的SDK,目前最新版本为3.0.4

  • 将下载好的压缩包解压,并将解压后的flutter文件夹移动到想要安装的目录中


    当前安装目录路径为:~/Documents/flutter

1.3. 下载安装Android Studio

  • 下载链接

    直接点击Download Android Studio,弹出协议勾选同意,当前环境为M1,所以点击下载Mac with Apple chip,如果不是M系列处理器,则下载Mac with Intel chip。

  • 安装好后,打开Android Studio,选择标准模式。
  • 选择同意所有协议,点击Finish
  • 开始下载相关组件
  • 出现错误,经查阅应该是网络问题,点击Retry后成功
  • 添加Flutter和Dart插件,Android Studio打开Preferences下载Dart和Flutter插件,如下如所示打开设置:

1.4. 配置环境变量:

  • 打开终端,当前终端默认为zsh模式
  • 在终端输入命令:vim ~/.zshrc(如果当前终端模式bash,则为vim ~/.bash_profile,后续操作也是以~/.bash_profile为准)
  • 按【i】进入编辑模式,将以下内容保存添加进去:
export PATH=~/Documents/flutter/bin:$PATH
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

其中export PATH = 此处为flutter库文件安装路径/bin:$PATH。

  • 添加完以后按esc退出编辑模式,再输入:wq进行保存。
  • 然后再运行命令使.zshrc文件生效:source ~/.zshrc

1.5. 测试Flutter环境

  • 在终端运行命令:flutter doctor
  • 如果此时报错:env: bash: No such file or directory。则说明export PATH的路径设置不对,检查配置正确后即可。
  • 如果配置正确,运行结果如下:

1.6. 解决flutter doctor(flutter运行环境检测)的问题

  • 按提示的执行命令flutter doctor --android-licenses,出现报错
    解决方法:在Android Studio的设置中勾选下图中的Android SDK Command-line Tools,并应用
  • 再次执行命令成功

    一直选y即可

  • 再次执行flutter doctor,全部正常。至此Flutter环境安装成功。

2. 创建Flutter模块项目

2.1. Flutter模块项目创建

  • cd到指定目录,用命令创建项目:flutter create -t module cmcc_test_module
    注意:flutter项目中的所有文件命名包括工程名,默认不使用驼峰命名法(有些地方如果用驼峰会直接被禁止创建),一般全部小写,以下划线分割。
  • 将项目文件夹拖动到Android Studio上,即可打开项目。打开项目后,引入相关依赖后,点击Pub get,其中ref一定要使用空安全版本。具体版本可以到:https://github.com/alibaba/flutter_boost进行查看
flutter_boost:
  git:
   url: 'https://github.com/alibaba/flutter_boost.git'
   ref: 'v3.0-null-safety-release.2.1'
  • 在工程的插件中,已经可以看到flutter boost了。

2.2. Flutter模块项目代码

  • 新建home.dart,用于纯跳转
import 'package:flutter/material.dart';

/// flutter首页
class CMHomePage extends StatelessWidget {
  const CMHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: TextButton(
            onPressed: () => _pushNativeViewController(),
            child: const Text("首页跳转原生界面")
        ),
      ),
    );
  }

  // 跳转原生界面
  _pushNativeViewController() {
    BoostNavigator.instance.push("TestViewController");
  }

}
  • 新建mine.dart,用于传参跳转
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

/// flutter我的
class CMMinePage extends StatefulWidget {
  final String data;
  const CMMinePage({Key? key, required this.data}) : super(key: key);

  @override
  State<CMMinePage> createState() => _CMMinePageState();
}

class _CMMinePageState extends State<CMMinePage> {
  MethodChannel eventChannel = const MethodChannel('com.flutterToNative.test');

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text("flutter我的,传输数据为:${widget.data}")
      ),
    );
  }
  
}
  • 在main.dart中配置路由表
import 'package:cmcc_test_module/home.dart';
import 'package:flutter/material.dart';
import 'package:flutter_boost/flutter_boost.dart';
import 'mine.dart';
import 'home.dart';

// BoostFlutterBinding用于接管Flutter App的生命周期,必须得接入的
class CustomFlutterBinding extends WidgetsFlutterBinding with BoostFlutterBinding {
}

void main() {
  CustomFlutterBinding();
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  ///路由表
  static Map<String, FlutterBoostRouteFactory> routerMap = {
    'homePage': (settings, uniqueId) {
      return PageRouteBuilder<dynamic>(
          settings: settings,
          pageBuilder: (_, __, ___) {
            return const CMHomePage();
          });
    },
    'minePage': (settings, uniqueId) {
      return PageRouteBuilder<dynamic>(
          settings: settings,
          pageBuilder: (_, __, ___) {
            Map<String, dynamic>? map = settings.arguments as Map<String, dynamic>?;
            String data = map?['data'] as String ?? "";
            return CMMinePage(
              data: data,
            );
      });
    },
  };

  Route<dynamic>? routeFactory(RouteSettings settings, String? uniqueId) {
    FlutterBoostRouteFactory? func = routerMap[settings.name];
    if (func == null) {
      return null;
    }
    return func(settings, uniqueId);
  }

  Widget appBuilder(Widget home) {
    return MaterialApp(home: home, debugShowCheckedModeBanner: false);
  }

  @override
  Widget build(BuildContext context) {
    return FlutterBoostApp(
      routeFactory,
      appBuilder: appBuilder,
    );
  }

}
  • 至此Flutter工程代码编写完毕。

3. 集成Flutter工程到iOS工程中

  • 为了方便调试,先用新建的iOSDemo工程进行测试,这样方便排查问题,减少不必要的影响因素。把Flutter Module项目文件夹拖动到新建的iOSDemo工程的根目录中
  • 配置Podfile文件,并在终端执行pod install
platform:ios,'9.0'

flutter_application_path = './FlutterModule/cmcc_test_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'FlutterTest' do
install_all_flutter_pods(flutter_application_path)
end
  • pod install执行成功后,打开工程目录,在pod中发现flutter_boost以及module已导入
  • 在iOSDemo工程中新建:BoostDelegate
    BoostDelegate.h
//
//  BoostDelegate.h
//  FlutterTest
//
//  Created by mac on 2022/7/13.
//
//  原生界面跳转Flutter界面代理

#import <Foundation/Foundation.h>
#import <flutter_boost/FlutterBoost.h>

NS_ASSUME_NONNULL_BEGIN

@interface BoostDelegate : NSObject<FlutterBoostDelegate>

// 设置导航控制器(外部必须设置,否则会导致无法跳转)
@property (nonatomic, strong) UINavigationController *navigationController;

// 创建单例
+ (instancetype)sharedInstance;

@end

NS_ASSUME_NONNULL_END

BoostDelegate.m

//
//  BoostDelegate.m
//  FlutterTest
//
//  Created by mac on 2022/7/13.
//

#import "BoostDelegate.h"

@implementation BoostDelegate

// 创建单例
+ (instancetype)sharedInstance{
    static BoostDelegate *myInstance = nil;
    if(myInstance == nil){
        myInstance = [[BoostDelegate alloc] init];
    }
    return myInstance;
}

// 跳转原生界面api,Flutter界面跳转原生界面,会自动调用此方法
-(void)pushNativeRoute:(NSString *)pageName arguments:(NSDictionary *)arguments {
    // 是否有动画
    BOOL animated = [arguments[@"animated"] boolValue];
    // 弹出方式
    BOOL present = [arguments[@"present"] boolValue];
    // 这里根据pageName来判断生成哪个vc
    UIViewController *vc;
    if ([pageName isEqualToString:@"TestViewController"]) {
        vc = [TestViewController new];
    }else{
        // 没有找到,则创建一个控制器作为容错
        vc = [UIViewController new];
    }
    if (present) {
        [self.navigationController presentViewController:vc animated:animated completion:^{
        }];
    } else {
        [self.navigationController pushViewController:vc animated:YES];
    }
}

// 当框架的withContainer为true的时候,会调用此方法来做原生的push
-(void)pushFlutterRoute:(FlutterBoostRouteOptions *)options {
    // 创建Flutter控制器
    FBFlutterViewContainer *vc = FBFlutterViewContainer.new;
    // 设置页面名称,参数等信息
    [vc setName:options.pageName uniqueId:options.uniqueId params:options.arguments opaque:options.opaque];
    // 是否有动画
    BOOL animated = [options.arguments[@"animated"] boolValue];
    // 弹出方式
    BOOL present = [options.arguments[@"present"] boolValue] || !options.opaque;
    if (present) {
        [self.navigationController presentViewController:vc animated:animated completion:^{
            options.completion(YES);
        }];
    } else {
        [self.navigationController pushViewController:vc animated:animated];
        options.completion(YES);
    }
}

// 当pop调用涉及到原生容器的时候,此方法将会被调用
-(void)popRoute:(FlutterBoostRouteOptions *)options {
    // 取出控制器
    FBFlutterViewContainer *vc = (id)self.navigationController.presentedViewController;
    // 判断控制器是否为Flutter控制器
    if ([vc isKindOfClass:FBFlutterViewContainer.class] && [vc.uniqueIDString isEqual:options.uniqueId]) {
        if (vc.modalPresentationStyle == UIModalPresentationFullScreen) {
            [self.navigationController.topViewController beginAppearanceTransition:YES animated:NO];
            [vc dismissViewControllerAnimated:YES completion:^{
                [self.navigationController.topViewController endAppearanceTransition];
            }];
        } else {
            [vc dismissViewControllerAnimated:YES completion:^{
            }];
        }
    } else {
        [self.navigationController popViewControllerAnimated:YES];
    }
}
@end
  • appDelegate中注册FlutterBoost
    AppDelegate.m
//
//  AppDelegate.m
//  FlutterTest
//
//  Created by mac on 2022/7/13.
//

#import "AppDelegate.h"
#import <FlutterPluginRegistrant/FlutterPluginRegistrant-umbrella.h>
#import "BoostDelegate.h"
#import <flutter_boost/FlutterBoost.h>
#import "ViewController.h"

@interface AppDelegate ()

@end

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 初始化window
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    ViewController *vc = [ViewController new];
    UINavigationController *navigation = [[UINavigationController alloc] initWithRootViewController:vc];
    self.window.rootViewController = navigation;
    [self.window makeKeyAndVisible];
    self.window.backgroundColor = [UIColor whiteColor];
    
    // 初始化FlutterBoost
    BoostDelegate *delegate = [BoostDelegate sharedInstance];
    delegate.navigationController = navigation;
    [[FlutterBoost instance] setup:application delegate:delegate callback:^(FlutterEngine *engine) {
    }];
    return YES;
}

@end
  • 在根控制器ViewController中进行跳转
    ViewController.m
//
//  ViewController.m
//  FlutterTest
//
//  Created by mac on 2022/7/13.
//

#import "ViewController.h"
#import <flutter_boost/FlutterBoost.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    UIButton *pushHomePageButton = [UIButton buttonWithType:UIButtonTypeSystem];
    pushHomePageButton.frame = CGRectMake(100, 200, 200, 100);
    [pushHomePageButton setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
    [pushHomePageButton setTitle:@"跳转到Flutter首页" forState:UIControlStateNormal];
    [pushHomePageButton addTarget:self action:@selector(pushFlutterHomePage) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:pushHomePageButton];
    
    UIButton *pushMinePageButton = [UIButton buttonWithType:UIButtonTypeSystem];
    pushMinePageButton.frame = CGRectMake(100, 100, 200, 100);
    [pushMinePageButton setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
    [pushMinePageButton setTitle:@"跳转到Flutter我的" forState:UIControlStateNormal];
    [pushMinePageButton addTarget:self action:@selector(pushFlutterMinePage) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:pushMinePageButton];
}

// 跳转Flutter首页
- (void)pushFlutterHomePage {
    FlutterBoostRouteOptions *options = [FlutterBoostRouteOptions new];
    // 此处填写的页面名称,需要在Flutter Module项目中main的路由表中有对应的路由名称,否则会导致匹配不上跳转失败
    options.pageName = @"homePage";
    options.arguments = @{@"animated": @(YES)};
    options.completion = ^(BOOL completion) {
    };
    [[FlutterBoost instance] open:options];
    options.onPageFinished = ^(NSDictionary *dic) {
        NSLog(@"%@", dic);
    };
}

// 跳转Flutter我的
- (void)pushFlutterMinePage {
    FlutterBoostRouteOptions *options = [FlutterBoostRouteOptions new];
    options.pageName = @"minePage";
    options.arguments = @{@"animated": @(YES), @"data": @"原生界面参数传递"};
    options.completion = ^(BOOL completion) {
    };
    [[FlutterBoost instance] open:options];
    options.onPageFinished = ^(NSDictionary *dic) {
        NSLog(@"%@", dic);
    };
}

@end
  • 新建测试页面,用于Flutter界面跳转原生界面。
    TestViewController.m
//
//  TestViewController.m
//  FlutterTest
//
//  Created by mac on 2022/7/18.
//

#import "TestViewController.h"

@interface TestViewController ()

@end

@implementation TestViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor redColor];
}

@end
  • 运行项目,跳转成功。
    Flutter界面跳转到原生界面

附:Demo下载

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

推荐阅读更多精彩内容