将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,如果未安装可以参考:
    • Homebrew安装
    • CocoaPods安装

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 createState() => _CMMinePageState();
}

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

  Route? 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 
#import 

NS_ASSUME_NONNULL_BEGIN

@interface BoostDelegate : NSObject

// 设置导航控制器(外部必须设置,否则会导致无法跳转)
@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 
#import "BoostDelegate.h"
#import 
#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 

@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下载

你可能感兴趣的:(将Flutter Module集成到iOS工程中)