Flutter 三端分离模式开发(先基于原有iOS项目开发,更新中···)

2020年之前有些教程创建方式已经不适用,要么缺文件、要么原生项目运行报错,折腾几天半个月都不行,还是官方文档教程比较靠谱。

目前原生App和Flutter混合开发有两种模式:

  • 统一管理模式:将原生工程作为Flutter工程的子工程,由Flutter进行统一管理。
  • 三端分离模式:将Flutter工程作为原生工程的子模块,维持原有的原生工程管理方式不变。

一、基于原有iOS项目集成Flutter框架

前提要拥有CocoaPods和Flutter环境配置
Flutter官方文档

1.1、创建项目文件夹

新建总项目文件夹,存放三端项目文件如:carry_sniper

1.2、创建Flutter模块

在总项目文件夹内,创建Flutter模块工程如:flutter_module

cd xxx/carry_sniper 你的文件夹路径
flutter create --template module flutter_module

执行结果:

Creating project flutter_module...
  flutter_module/test/widget_test.dart (created)
  flutter_module/flutter_module.iml (created)
  flutter_module/.gitignore (created)
  flutter_module/.metadata (created)
  flutter_module/pubspec.yaml (created)
  flutter_module/README.md (created)
  flutter_module/lib/main.dart (created)
  flutter_module/flutter_module_android.iml (created)
  flutter_module/.idea/libraries/Dart_SDK.xml (created)
  flutter_module/.idea/modules.xml (created)
  flutter_module/.idea/workspace.xml (created)
Running "flutter pub get" in flutter_module...                      0.8s
Wrote 11 files.

All done!
Your module code is in flutter_module/lib/main.dart.
1.3、创建iOS项目工程

在总项目文件夹,使用Xcode创建iOS项目工程如:CarrySniperiOS

1.4、关联操作
1.4.1为iOS工程添加CocoaPods依赖,生成Proflie文件

终端指令执行:

cd xxx/carry_sniper/CarrySniperiOS 你的iOS工程项目路径
pod init
1.4.2打开Proflie文件添加、修改内容

主要2段3行代码,注意代码存放位置,和flutter_module名称一致

flutter_application_path = '../flutter_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
install_all_flutter_pods(flutter_application_path)

配置结果如下:

platform :ios, '11.0'
# 1、Flutter模块加入
flutter_application_path = '../flutter_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'CarrySniperiOS' do
  use_frameworks!
  # 2、安装嵌入Flutter模块
  install_all_flutter_pods(flutter_application_path)

  # Pods for CarrySniperiOS

end
1.4.3执行指令,完成Flutter模块的添加和CocoaPods依赖
pod install

执行结果:
要看到Installing Flutter相关依赖的安装,否则运行报错
关闭当前Xcode项目,从此使用CarrySniperiOS.xcworkspac运行工程

/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/universal-darwin19/rbconfig.rb:229: warning: Insecure world writable dir /Users/Macbook/Documents/FlutterSDK/flutter/bin in PATH, mode 040777
Analyzing dependencies
Downloading dependencies
Installing Flutter (1.0.0)
Installing FlutterPluginRegistrant (0.0.1)
Installing flutter_module (0.0.1)
Generating Pods project
Integrating client project

[!] Please close any current Xcode sessions and use `CarrySniperiOS.xcworkspace` for this project from now on.
Pod installation complete! There are 3 dependencies from the Podfile and 3 total pods installed.
1.5、完成基础配置

可以直接打开项目运行,没问题就说明配置成功。部分目录如下:

carry_sniper/
├── CarrySniperiOS/
│   ├── CarrySniperiOS/
│   ├── Pods/
│   ├── Podfile
│   ├── Podfile.lock
│   ├── CarrySniperiOS.xcodeproj
│   └── CarrySniperiOS.xcworkspace
├── flutter_module/
│   ├── .android/
│   ├── .ios/
│   │    ├── Runner.xcworkspace
│   │    └── Flutter/podhelper.rb
│   ├── lib/
│       └── main.dart
│   ├── flutter_module_android.iml
│   ├── flutter_module.iml
│   ├── pubspec.lock
│   ├── test/
│   └── pubspec.yaml

二、原生iOS项目调用Flutter

根据官方Flutter文档,进行简单的原生app调用Flutter,列举2种方法,这里是Objective-C代码,文档有Swift代码。更高级的调用方法可以继续看文档。

2.1方式一:使用FlutterViewController

直接在ViewController.m文件编写代码,运行项目即可:

#import "ViewController.h"
#import <Flutter/Flutter.h>

@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button addTarget:self action:@selector(showFlutter)
     forControlEvents:UIControlEventTouchUpInside];
    [button setTitle:@"Show Flutter!" forState:UIControlStateNormal];
    button.backgroundColor = UIColor.blueColor;
    button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);
    [self.view addSubview:button];
}

- (void)showFlutter {
    FlutterViewController *flutterViewController = [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
    [self presentViewController:flutterViewController animated:YES completion:nil];
}

@end
2.2、方式二:使用FlutterEngine
2.1.1、依赖FlutterAppDelegate创建一个实体FlutterEngine

AppDelegate.h文件内容:

#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>

@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 *)launchOptions {
    self.flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"];
    // Runs the default Dart entrypoint with a default Flutter route.
    [self.flutterEngine run];
    // Used to connect plugins (only if you have plugins with iOS platform code).
    [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
    return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
2.2.2、使用FlutterEngine调用Flutter页面和传参

ViewController.m文件内容:

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

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    // Make a button to call the showFlutter function when pressed.
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button addTarget:self action:@selector(showFlutter)
     forControlEvents:UIControlEventTouchUpInside];
    [button setTitle:@"Show Flutter!" forState:UIControlStateNormal];
    button.backgroundColor = UIColor.blueColor;
    button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);
    [self.view addSubview:button];
}

- (void)showFlutter {
    FlutterEngine *flutterEngine =
    ((AppDelegate *)UIApplication.sharedApplication.delegate).flutterEngine;
    FlutterViewController *flutterViewController =
    [[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil];
    [self presentViewController:flutterViewController animated:YES completion:nil];
}

@end
2.2.3、运行Xcode中的CarrySniperiOS项目

三、原生iOS项目也能使用热更新、热重载

官方文档:
要先安装homebrew,运行Xcode项目,保证原生App有安装到手机/模拟器上,然后在Android Studio的Flutter项目跟路径执行指令:

flutter attach

如果有多个设备(或者输入下标选择相应设备):

flutter attach -d xxxxx设备id

如果有多个包名:

那么需要统一Android和iOS的包名后再试一遍

如果出现Waiting for a connection from Flutter on iPhone ...一直等待,

说明原生App没有启动,控制台连接不到设备应用运行。
需要手动到手机/模拟器点击运行App即可(只需运行Android Studio,不需要打开Xcode软件,Xcode会在Flutter下默默运行。当然不打开Xcode,就看不到控制台打印输出,但影响不大)。

执行结果:

Waiting for iPhone 12 Pro to report its views...                     7ms
Syncing files to device iPhone 12 Pro...                                
 8,140ms (!)                                       

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h Repeat this help message.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).
An Observatory debugger and profiler on iPhone 12 Pro is available at: http://127.0.0.1:51810/ucfjPzF2_sY=/

四、原生iOS项目后续可能遇到的问题

  • 如果发现运行App进度和Flutter开发进度不一致,需要去Xcode运行原生项目App。需要保证打包的代码是最新的,否则安装的App再次启动后,永远是之前的版本,没有包含最新Flutter部分的代码。
个人理解:已安装的App,我们使用饭flutter attach,热更新和热重载只保证运行时代码是最新的,运行结束之后就恢复原生App安装时的模样,并没有把最新的Flutter代码打包到安装包里面。
  • Showing Recent Messages Undefined symbol: protocol conformance descriptor fo xxx 等几十上百个报错
莫名其妙的出现,之前运行还好好的,可能Flutter添加了某些package,在原生项目就突然出问题了。
可能解决方法:
尝试一:升级CocoaPods;
尝试二:更新依赖库 pod install --verbose --no-repo-update
尝试三:为原生项目添加swift桥接文件,任意直接New一个.swift文件,Xcode 提示 Create Bridging Header ,选择创建即可。记得.swift文件保留不删除。
  • Command PhaseScriptExecution failed with a nonzero exit code
  • /packages/flutter_tools/bin/xcode_backend.sh: No such file or directory
先检查Flutter SDK里面存不存在xcode_backend.sh文件,不存在就去找一个或者重新下载sdk;
存在的话,可能就是Xcode项目配置可能缺少FLUTTER_ROOT,Target -> Build Setting -> User-Defined 添加FLUTTER_ROOT对应sdk路径;
如果不知道路径可以直接复制flutter项目的.ios的Generated.xcconfig里面的FLUTTER_ROOT内容,会自动帮导入到User-Defined。
  • Support for empty structs is deprecated and will be removed in the next stable version of Dart. Use Opaque instead.
遇到SDK版本更新,API过期更替。不影响使用,需要等待第三方插件更新支持最新包。可以用'flutter downgrade'指令降级SDK。

五、一些想法

三端分离,flutter里面iOS的info.plist文件很容易在安卓合并代码或pub get指令执行时会被重置,影响开发和调试。当然原生项目的info.plist文件是不影响的,只是不想开发期间直接用原生项目,效率不一样。

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

推荐阅读更多精彩内容