[混编] iOS原生项目- 接入Flutter

4,178 阅读7分钟

1. 

  • Flutter Application: Flutter应用                 包含标准的Dart层与Native平台层
  • Flutter Module :          Flutter与原生混合开发
  • Flutter Plugin:           Flutter插件
  • Flutter Package:       纯Dart组件

还是有点不够清楚

Flutter Application  创建的是纯flutterApp
Flutter Plugin          用于为flutterAPP创建三方或者工具插件

Flutter Module 用于创建安卓ios平台原生上的组件 flutter  Module

Flutter Package   纯Dart组件 无平台区分的

2. iOS原生App如何接入flutter作为部分功能模块「flutter官方方案」

简单说两种:

1. 创建flutter module,通过CocoaPods集成到xcode项目里

    生成两个framework,Flutter.framework 和 App.framework

2. 创建flutter module,编译后 手动引入xcode改成 Framework的动态库

一般项目都集成有cocoapods 所以都选择1;

方式1具体如何
1. 生成flutter module

cd some/path/
flutter create --template module my_flutterm


目录结构

my_flutter/

├── .ios/
│   ├── Runner.xcworkspace
│   └── Flutter/podhelper.rb
├── lib/
│   └── main.dart
├── test/
└── pubspec.yaml

2. 

  1. Add the following lines to your Podfile:

    content_copy

    flutter_application_path = '../my_flutter'   <-相对路径具体,我是同级目录
    load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
    
  2. For each Podfile target that needs to embed Flutter, call install_all_flutter_pods(flutter_application_path).

    content_copy

    target 'MyApp' do
      install_all_flutter_pods(flutter_application_path)
    end
    
  3. Run pod install.

3. 代码层面实现方式1 启动即启动FlutterEngine

import Flutterimport FlutterPluginRegistrant@main
//这里FlutterAppDelegate 在 UIResponder <UIApplicationDelegate>前面加了一层 //用于自己的接收系统事件,完成自定实现 class AppDelegate: FlutterAppDelegate {    lazy var flutterEngine = FlutterEngine(name: "my flutter engine")    override func application(_ application: UIApplication, didFinishLaunchingWithOptions 
                launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {        flutterEngine.run();        GeneratedPluginRegistrant.register(with: self.flutterEngine);        return super.application(application, didFinishLaunchingWithOptions: launchOptions);    }}

@IBAction func openFlutterPage(_ sender: Any) { 
//展示flutter main 里面的页面  let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine  let flutterViewController =   FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)        present(flutterViewController, animated: true, completion: nil)    }

注意:

UIApplicationDelegate subclass FlutterAppDelegate is recommended but not required
这里的继承是推荐的,但不是必须的。
make your app delegate implement the FlutterAppLifeCycleProvider protocol in order to make sure your plugins receive the necessary callbacks

让AppDelegate 去实现这个协议, 在需要通知flutter的具体回调里发送通知

**

实现方式2 作为前面示例的替代方案,您可以让FlutterViewController隐式地创建自己的FlutterEngine,而不需要提前预热。**

// Existing code omitted.
func showFlutter() {
  let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil)
  present(flutterViewController, animated: true, completion: nil)
}

FlutterEngine, by default, runs the main()Dart function of your lib/main.dart file.

You can also run a different entrypoint function by using runWithEntrypoint with an NSString specifying a different Dart function.

引擎默认打开的是main.dart的page,你也可以通过 [runWithEntrypoint](https://api.flutter.dev/objcdoc/Classes/FlutterEngine.html#/c:objc(cs)FlutterEngine(im)runWithEntrypoint:)  开启一个自定义新的入口页面

Note: Dart entrypoint functions other than main() must be annotated with the following in order to not be tree-shaken away when compiling:

  @pragma('vm:entry-point')
  void myOtherEntrypoint() { ... };

使用
flutterEngine.run(withEntrypoint: "myOtherEntrypoint", 
                       libraryURI: "other_file.dart")

Route路由

let flutterEngine = FlutterEngine()
// FlutterDefaultDartEntrypoint is the same as nil, which will run main().
engine.run(
  withEntrypoint: FlutterDefaultDartEntrypoint, initialRoute: "/onboarding")

这话的意思是,让Dart:UIwindows.defaultRouteName 变成自定义/onboadrding 而不是 /

Alternatively, to construct a FlutterViewController directly without pre-warming a FlutterEngine.  或者 构建FlutterViewController直接跳过预热flutter引擎

FlutterViewController* flutterViewController =
      [[FlutterViewController alloc] initWithProject:nil
                                        initialRoute:@"/onboarding"
                                             nibName:nil
                                              bundle:nil];

4. 第二种方案: 将flutter以framework的形式通过Cocoapods引入iOS工程

这也是我们本篇的主要内容 其中 Cocoapods引入也分为两种方式:

  1. pod的本地路径化引入

  2. pod通过远程Git仓库引入 我们先来介绍本地化引入

一、 pod的本地化引入
$ cd ~/Desktop/FlutterForFW/iOSProject
$ pod init
$ pod install

1.2. 接下来创建名字为‘ MyFlutterPod’的Pod库

$ cd ~/Desktop/FlutterForFW
$ pod lib create MyFlutterPod

终端依次输入所需类型:

xingkunkun:FlutterForFW admin$ pod lib create MyFlutterPod
Cloning `https://github.com/CocoaPods/pod-template.git` into `MyFlutterPod`.
Configuring MyFlutterPod template.
------------------------------
To get you started we need to ask a few questions, this should only take a minute.

What platform do you want to use?? [ iOS / macOS ]
 > ios
What language do you want to use?? [ Swift / ObjC ]
 > objc
Would you like to include a demo application with your library? [ Yes / No ]
 > no
Which testing frameworks will you use? [ Specta / Kiwi / None ]
 > none
Would you like to do view based testing? [ Yes / No ]
 > no
What is your class prefix?
 > Kevin

Running pod install on your new library.

创建完成之后会有一个工程自动打开,此工程为Pod工程,在Example->MyFlutterPod.xcworkspace打开后可以作为独立项目在此编码iOS代码之类的,暂时先不在此进行编写原生代码,关闭退出。

1.3. 在MyFlutterPod目录下创建 Flutter Module模块

$ cd ~/Desktop/FlutterForFW/MyFlutterPod
$ flutter create -t module flutter_module_for_ios

命令执行完后,目录文件夹下会多出一个名为flutter_module_for_ios的flutter模板项目

该项目模板包含有flutter代码模块+隐藏.ios文件。同时选中三个键可以使隐藏文件显示

command + shift + .

在当前flutter_module_for_ios文件lib中可以编码flutter相关代码,考虑到可能会在flutter项目中使用到相关插件,我们可以在pubspec.yaml中添加一个插件

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2
  #添加 数据持久化插件  https://pub.flutter-io.cn/packages/shared_preferences
  shared_preferences: ^0.5.4+3

1.4、在flutter_module_for_ios项目中执行安装插件操作

$ cd ~/Desktop/FlutterForFW/MyFlutterPod/flutter_module_for_ios
$ flutter pub get

可以看到在.ios文件夹下自动生成出来一个Podfile文件

1.5、执行编译该flutter_module_for_ios项目

编译后会生成Flutter所依赖的相关的库文件。我们在当前先编译出debug版本的库文件方便我们后续调试

$ flutter build ios --debug      //编译debug产物
或者
$ flutter build ios --release --no-codesign //编译release产物(选择不需要证书)复制代码

观察项目中的变化,可发现有多出编译产物

我们所需要的就是这些生成出来的framework库

build目录下

ios->Debug-iphoneos-> FlutterPluginRegistrant.framework

ios->Debug-iphoneos-> shared_preferences.framework

.ios目录下

Flutter-->App.framework Flutter-->engine-->Flutter.framework

当前生成的库都是debug版本库文件。 需要注意的是,后续若想编译出release版本的framework库,修改下面的脚本文件根据注释提示修改。因为在build生成产物之前会先重置文件为初始状态

接下来iOS工程通过Pod把这些库引入到自己的工程中了。为了方便集中快速管理操作我们可以通过创建脚本的方式对其进行管理(思路就是通过脚本创建一个文件夹,将这些散乱在各文件的库统一拷贝进来)

2.1、在flutter_module_for_ios下创建脚本文件

$ cd ../flutter_module_for_ios
$ touch move_file.sh   //1. 创建脚本文件
$ open move_file.sh    //2. 打开脚本文件复制代码

添加以下脚本代码

if [ -z $out ]; then
    out='ios_frameworks'
fi

echo "准备输出所有文件到目录: $out"

echo "清除所有已编译文件"
find . -d -name build | xargs rm -rf
flutter clean
rm -rf $out
rm -rf build

flutter packages get

addFlag(){
    cat .ios/Podfile > tmp1.txt
    echo "use_frameworks!" >> tmp2.txt
    cat tmp1.txt >> tmp2.txt
    cat tmp2.txt > .ios/Podfile
    rm tmp1.txt tmp2.txt
}

echo "检查 .ios/Podfile文件状态"
a=$(cat .ios/Podfile)
if [[ $a == use* ]]; then
    echo '已经添加use_frameworks, 不再添加'
else
    echo '未添加use_frameworks,准备添加'
    addFlag
    echo "添加use_frameworks 完成"
fi

echo "编译flutter"
flutter build ios --debug
#release下放开下一行注释,注释掉上一行代码
#flutter build ios --release --no-codesign
echo "编译flutter完成"
mkdir $out
cp -r build/ios/Debug-iphoneos/*/*.framework $out
#release下放开下一行注释,注释掉上一行代码
#cp -r build/ios/Release-iphoneos/*/*.framework $out
cp -r .ios/Flutter/App.framework $out
cp -r .ios/Flutter/engine/Flutter.framework $out

echo "复制framework库到临时文件夹: $out"

libpath='../'

rm -rf "$libpath/ios_frameworks"
mkdir $libpath
cp -r $out $libpath

echo "复制库文件到: $libpath"复制代码

注意观察脚本文件中的代码意思:将编译生成的debug版本的所需.framework库文件拷贝至ios_frameworks文件下并复制一份到MyFlutterPod目录下,后续若想编译生成release版本库文件时还需修改脚本文件查找对应上release标识

2.2、执行脚本文件

$ sh move_file.sh      //3. 执行脚本文件复制代码

此时的ios_frameworks文件已经生成拷贝

里面包含有我们前面提到所需要的.framework所有库文件

接下来我们就要通过MyFlutterPod库的podspec来创建依赖导出

3.1、编辑podspec文件

打开podspec文件在end前一行添加以下命令

  s.static_framework = true
  p = Dir::open("ios_frameworks")
  arr = Array.new
  arr.push('ios_frameworks/*.framework')
  s.ios.vendored_frameworks = arr复制代码

添加之后文件整体长这样

3.2、在iOSProject项目的podfile文件中执行pod引用

在iOSProject工程下的podfile文件中添加

# Uncomment the next line to define a global platform for your project
platform :ios, '8.0'

target 'iOSProject' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for iOSProject
   pod 'MyFlutterPod', :path => '../MyFlutterPod'

end复制代码

之后执行

$ pod install

可以看到终端提示安装MyFlutterPod库成功

其中MyFlutterPod库里就包含有我们所需的上述提到的framework库

OK下面我们来试一下如何在iOS项目中跳转进flutter界面,也就是我们提到的混合开发的代码测试,基本上也就是按照官方提供的模板写

4.1、AppDelegate.h中修改

//  AppDelegate.h
//  iOSProject


#import 
#import 

@interface AppDelegate : FlutterAppDelegate
@property (nonatomic,strong) UIWindow *window;

@end复制代码

4.2、AppDelegate.m中修改

//  AppDelegate.m
//  FlutterPodTest

#import "AppDelegate.h"
#import "ViewController.h"
#import 

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    if (@available(iOS 13.0, *)) {
        
    } else {
        self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
        [self.window setBackgroundColor:[UIColor whiteColor]];
        ViewController *con = [[ViewController alloc] init];
        UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:con];
        [self.window setRootViewController:nav];
        [self.window makeKeyAndVisible];
        
    }
       [GeneratedPluginRegistrant registerWithRegistry:self];
    
    return YES;
}复制代码

4.3、SceneDelegate.m

#import "SceneDelegate.h"
#import "ViewController.h"

@implementation SceneDelegate

- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
       //在这里手动创建新的window
        if (@available(iOS 13.0, *)) {
            UIWindowScene *windowScene = (UIWindowScene *)scene;
            self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
            [self.window setWindowScene:windowScene];
            [self.window setBackgroundColor:[UIColor whiteColor]];
            
            ViewController *con = [[ViewController alloc] init];
            UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:con];
            [self.window setRootViewController:nav];
            [self.window makeKeyAndVisible];
        }
}复制代码

4.4、ViewController.m

//
//  ViewController.m
//  iOSProject


#import "ViewController.h"
#import "AppDelegate.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button setFrame:CGRectMake(100, 100, 200, 50)];
    [button setBackgroundColor:[UIColor lightGrayColor]];
    [button setTitle:@"ClickMePushToFlutterVC" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(btn_click) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
    
}

- (void)btn_click {
    
    FlutterViewController *flutterViewController = [[FlutterViewController alloc] init];
    [self.navigationController pushViewController:flutterViewController animated:YES];

    /* 方式 2
     
    FlutterViewController *fluvc = [[FlutterViewController alloc]init];
    [self addChildViewController:fluvc];
    fluvc.view.frame = self.view.bounds;
    [fluvc didMoveToParentViewController:self];
    [self.view addSubview:fluvc.view];
    [self.navigationController pushViewController:fluvc animated:YES];
     
     */
}复制代码

集成代码较官方方式有部分不同,这里没有通过 [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil]; 这种方式去初始化引擎,是因为FlutterViewContorller在new的时候会自动的创建一个引擎。而通过官方的方式去初始化引擎则需将该引擎设置成一个全局单例去使用

至此。第一种形式的pod本地化引入工程就已经完成。但是我们发现一个问题那就是目前感觉好像还是没有能完全剥离一台电脑上没有flutter环境配置的情况下如何去引入flutter.framework等库文件,难道要手动拷贝么,这样也不是很符合开发的初衷,接下来我会给大家介绍一下如何将创建好的私有库上传至git去托管,然后其他开发同学直接通过Git命令去引入包,这样也就从根源上解决了模块化的剥离,更为干净利落

一、 pod通过远程Git仓库引入,这里我选择了GitLab

1.1、远程创建仓库MyFlutterPod

1.2、在MyFlutterPod项目中与远端建立连接

$ cd ../MyFlutterPod
$ git remote add origin https://gitlab.com/OmgKevin/myflutterpod.git复制代码

为了防止上传文件过大的限制,可以选择在.gitignore文件中选择不上传flutter_module_for_ios代码,只将ios_frameworks文件中的库文件上传就好

1.2.1、gitignore文件

$ git add .
$ git commit -m "Initial commit"
$ git push -u origin master
// 给当前代码设置tag版本
$ git tag -m "first demo" 0.1.0
$ git push --tags复制代码

可能会有上传文件大小限制,解除具体可以参考这篇文章

www.jianshu.com/p/3b86486bc…

1.3、修改MyFlutterPod.podspec文件

需要注意的地方时你自己创建的gitlab地址与管理员邮箱及tag版本一一对应上

将此修改的文件推至远端仓库

$ git status
$ git add MyFlutterPod.podspec
$ git commit -m "修改文件"
$ git push origin master复制代码

1.4、验证一下Pod库文件是否可行

$ pod spec lint MyFlutterPod.podspec --verbose复制代码

1.5、在iOSProject文件中进行添加代码

如果在此之前做过本地化加载pod库,要先卸载掉之前安装过的文件 --1 注释掉podfile文件中的代码 pod 'MyFlutterPod', :path => '../MyFlutterPod' --2执行一下 pod install 可以看到之前安装过得库已经从项目中移除

修改podfile文件

# Uncomment the next line to define a global platform for your project
platform :ios, '8.0'

target 'iOSProject' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for iOSProject
#   pod 'MyFlutterPod', :path => '../MyFlutterPod'
   pod 'MyFlutterPod',:git=>'https://gitlab.com/OmgKevin/myflutterpod.git'

end复制代码

安装过程可能会比较慢,这跟网络有关

1.6、下载完毕的项目目录下可以看到添加进的framework库文件

2.1、可以试一下按照方式一中的代码切换进flutter页面,这里就不贴代码了

至此,通过Git远程管理的flutter模块集成进iOS项目已经完成了,以后每次flutter模块代码有更新时,直接推向远端,iOS开发同学直接在podfile文件中进行拉取,后续可以考虑加上tag标识来进行拉取

优点: 对 Flutter 自身的构建流程改动较少并且较彻底第解决了本地耦合的问题; 解决了组件式开发的痛点,各自开发各自的代码,也不用要求每台电脑上都配置flutter环境

缺点: 集成方式上变得貌似更加繁琐,Flutter 内容的变动需要先同步到远程仓库再 同步到 Standalone 模式方能生效;且要各自打包维护iOS安卓的两套代码仓库供不同平台去拉取调用

PS. 闲鱼APP 最终选择了这个策略。