背景:
現在Flutter火熱蚣常,各個公司都會嘗試使用Flutter開發(fā)抵蚊,一般采用的都會是混合形式的開發(fā)贞绳,混合開發(fā)目前Flutter官方提供的不太完善冈闭、iOS和Android有差異性萎攒、接口也不同意、很麻煩货矮,也有一些大廠為了實現高效率高可用自己實現一套混合開發(fā)技術,比如閑魚的flutter_boost和哈羅的flutter_thrio。兩者之間優(yōu)缺點及設計原理劫灶、架構等后續(xù)再來具體分析本昏。接下我們先說下flutter_boost在iOS項目中的接入混合實踐枪汪。
準備工作:
1.Flutter SDK
為了便于后續(xù)我們flutter sdk版本的快速切換宿稀,我們需要安裝管理工具祝沸,方便我們在后續(xù)開發(fā)或者調試過程中快速切換版本。
這里推薦使用的Flutter SDK管理工具fvm,能快速切換版本,具體不做介紹卤唉,直接去查看文檔就可以竭恬,很詳細萍聊。
安裝自己所需的flutter sdk版本(我這里安裝的是1.17.1版本,因為需要跟下邊的flutter_boost版本對應):
2.flutter_boost框架
了解混合框架flutter_boost,首先需要知道我們需要使用他們的那個版本及版本對應的flutter sdk版本亭螟,比如:flutter_boost:v1.17.1-hotfixes 對應的flutter sdk:1.17.1 预烙。
正式接入:
一扁掸、flutter_module工程
1.創(chuàng)建空文件夾后,使用命令:
flutter create -t module flutter_module
如果iOS使用的是Swift開發(fā)使用命令:
flutter create -i swift -t module flutter_module
2.flutter_module創(chuàng)建完成后镀脂,打開flutter_module文件夾下pubspec.yaml
文件薄翅。如下:
注意:這里我用的flutter_boost的SDK最新版本:v1.17.1-hotfixes
3.cd到flutter_module文件夾下翘魄,執(zhí)行命令:
flutter packages get
包下載完成后flutter_module工程基本配置已經完成斋射。
二绩鸣、iOS原生工程
1.創(chuàng)建iOS項目
注意:iOS工程根目錄與flutter_module根目錄平級呀闻。
2.cd到iOS工程目錄下創(chuàng)建Podfile文件,執(zhí)行命令:
touch Podfile
生成文件垒手,然后可以使用vim Podfile
編輯或者直接open Podfile
或者直接打開文本編輯也可以科贬。
在文件中添加
flutter_application_path = '../flutter_module'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
install_all_flutter_pods(flutter_application_path)
3.執(zhí)行 pod install
下載依賴庫榜掌。
4.配置項目
如果是OC工程憎账,則不會要做什么額外處理胞皱,如果是Swift項目需要建個橋接文件FlutterHybridiOS-Bridging-Header.h
雾鬼,同時Build Sttings中需要配置該文件路徑。
注意:
(1).看其他博客出現過森渐,查看Build Phases中是否存在Flutter Build Script的腳本竟块,如果不存在需要添加對應腳本,但是目前我沒出現缺失的情況埠况。
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
三.代碼接入實踐
這塊可以直接下載flutter_boost官方例子OC,flutter_boost官方例子Swift查看具體實踐苦囱,其中OC例子是最全的乾蓬,這里只已Swift項目為基礎介紹基礎使用矛紫。
<1>.iOS平臺
- 1.創(chuàng)建實現flutter_boost路由類,實現FLBPlatform中的協(xié)議,和Flutter中也會有對應的方法來處理跳轉操作缓呛。(直接參考上邊官方的代碼類就行),即push模式跳轉痰憎、present模式跳轉铣耘、關閉頁面等方法蜗细。
import Foundation
//import flutter_boost
class PlatformRouterImp: NSObject, FLBPlatform {
func open(_ url: String, urlParams: [AnyHashable : Any], exts: [AnyHashable : Any], completion: @escaping (Bool) -> Void) {
//跳轉打開原生Native頁面
if (url == "native") {
self.openNativeVC(url, urlParams: urlParams, exts: exts)
return
}
var animated = false;
if exts["animated"] != nil{
animated = exts["animated"] as! Bool;
}
let vc = FLBFlutterViewContainer.init();
vc.setName(url, params: urlParams);
self.navigationController().pushViewController(vc, animated: animated);
completion(true);
}
func present(_ url: String, urlParams: [AnyHashable : Any], exts: [AnyHashable : Any], completion: @escaping (Bool) -> Void) {
var animated = false;
if exts["animated"] != nil{
animated = exts["animated"] as! Bool;
}
let vc = FLBFlutterViewContainer.init();
vc.setName(url, params: urlParams);
navigationController().present(vc, animated: animated) {
completion(true);
};
}
func close(_ uid: String, result: [AnyHashable : Any], exts: [AnyHashable : Any], completion: @escaping (Bool) -> Void) {
var animated = false;
if exts["animated"] != nil{
animated = exts["animated"] as! Bool;
}
let presentedVC = self.navigationController().presentedViewController;
let vc = presentedVC as? FLBFlutterViewContainer;
if vc?.uniqueIDString() == uid {
vc?.dismiss(animated: animated, completion: {
completion(true);
});
}else{
self.navigationController().popViewController(animated: animated);
}
}
private func openNativeVC(
_ name: String?,
urlParams params: [AnyHashable : Any]?,
exts: [AnyHashable : Any]?
) {
let vc = UIViewController()
let animated = (exts?["animated"] as? NSNumber)?.boolValue ?? false
if (params?["present"] as? NSNumber)?.boolValue ?? false {
self.navigationController().present(vc, animated: animated) {}
} else {
self.navigationController().pushViewController(vc, animated: animated)
}
}
func navigationController() -> UINavigationController {
let delegate = UIApplication.shared.delegate as! AppDelegate
let navigationController = delegate.window?.rootViewController as! UINavigationController
return navigationController;
}
}
- 2.flutter_boost初始化吊骤,即在啟動的時候初始化框架代碼白粉。
import UIKit
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
//路由創(chuàng)建
let router = PlatformRouterImp.init();
//FlutterBoost初始化
FlutterBoostPlugin.sharedInstance().startFlutter(with: router, onStart: { (engine) in
});
self.window = UIWindow.init(frame: UIScreen.main.bounds)
let viewController = ViewController.init()
let navi = UINavigationController.init(rootViewController: viewController)
self.window.rootViewController = navi
self.window.makeKeyAndVisible()
return true
}
- 3.具體使用(創(chuàng)建自己的ViewController,添加兩個按鈕進行測試跳轉Native)
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
// Do any additional setup after loading the view.
let btn = UIButton(type: .custom);
btn.backgroundColor = UIColor.red
btn.frame = CGRect(x: 10, y: 60, width: 60, height: 40)
btn.setTitle("Push Flutter Page", for: .normal)
self.view.addSubview(btn);
btn.addTarget(self, action: #selector(onClickPushFlutterPage), for: .touchUpInside);
let btn2 = UIButton(type: .custom);
btn2.backgroundColor = UIColor.blue
btn2.frame = CGRect(x: 10, y: 120, width: 60, height: 40)
self.view.addSubview(btn2);
btn2.setTitle("Present Flutter Page", for: .normal)
btn2.addTarget(self, action: #selector(onClickPresentFlutterPage), for: .touchUpInside);
}
@objc func onClickPushFlutterPage(_ sender: UIButton, forEvent event: UIEvent){
// self.navigationController?.navigationBar.isHidden = true
//其中這里的first鹃祖,在Flutter也會對應初始化的時候注冊該標識對應的Widget。
FlutterBoostPlugin.open("first", urlParams:[kPageCallBackId:"MycallbackId#1"], exts: ["animated":true], onPageFinished: { (_ result:Any?) in
print(String(format:"call me when page finished, and your result is:%@", result as! CVarArg));
}) { (f:Bool) in
print(String(format:"page is opened\(f)"));
}
}
@objc func onClickPresentFlutterPage(_ sender: UIButton, forEvent event: UIEvent){
FlutterBoostPlugin.present("second", urlParams:[kPageCallBackId:"MycallbackId#2"], exts: ["animated":true], onPageFinished: { (_ result:Any?) in
print(String(format:"call me when page finished, and your result is:%@", result as! CVarArg));
}) { (f:Bool) in
print(String(format:"page is presented"));
}
}
}
*注意點:
(1).調用FlutterBoostPlugin.open和present方法時候楷兽,傳遞的urlname(first、second)會在Flutter中注冊對應的Widget。
<2>.Flutter平臺
- 1.main.dart (可以理解為入口類却特,類似iOS中的Appdelete類)筛圆,實現Flutter_boost初始化和注冊Native對應的widget太援。
(1)初始化flutter_boost插件及注冊對應的widgets仙蛉。
(2)初始化路由router。
import 'package:flutter/material.dart';
import 'package:flutter_boost/flutter_boost.dart';
import 'simple_page_widgets.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
//(1)初始化注冊
FlutterBoost.singleton.registerPageBuilders(<String, PageBuilder>{
'first': (String pageName, Map<String, dynamic> params, String _) => FirstRouteWidget(),
'firstFirst': (String pageName, Map<String, dynamic> params, String _) =>
FirstFirstRouteWidget(),
'second': (String pageName, Map<String, dynamic> params, String _) => SecondRouteWidget(),
'secondStateful': (String pageName, Map<String, dynamic> params, String _) =>
SecondStatefulRouteWidget(),
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Boost example',
//(2)初始化路由
builder: FlutterBoost.init(postPush: _onRoutePushed),
home: Container(color: Colors.white));
}
//(2)
void _onRoutePushed(
String pageName,
String uniqueId,
Map<String, dynamic> params,
Route<dynamic> route,
Future<dynamic> _,
) {}
}
注意點:
(1)import 'simple_page_widgets.dart'
這個dart類中是實現了很多widget哀墓,比如first對應的FirstRouteWidget麸祷,second對應的SecondRouteWidget等阶牍。但是在實際開發(fā)中不要把widget放到一個Dart類種實現星瘾,降低代碼冗余琳状、代碼復雜度念逞、降低維護成本等翎承。
- 2.實現widget(具體可以查看官方類中的simple_page_widgets.dart)
下邊只展示我們之前對應的first和second。
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_boost/flutter_boost.dart';
import 'package:flutter_module/platform_view.dart'; //封裝的view
/// *********************** FirstRouteWidget*/
class FirstRouteWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() => _FirstRouteWidgetState();
}
class _FirstRouteWidgetState extends State<FirstRouteWidget> {
_FirstRouteWidgetState();
// flutter 側MethodChannel配置啊胶,channel name需要和native側一致
// static const MethodChannel _methodChannel = MethodChannel('flutter_native_channel');
// String _systemVersion = '';
// Future<dynamic> _getPlatformVersion() async {
// try {
// final String result = await _methodChannel.invokeMethod('getPlatformVersion');
// print('getPlatformVersion:' + result);
// setState(() {
// _systemVersion = result;
// });
// } on PlatformException catch (e) {
// print(e.message);
// }
// }
@override
void initState() {
print('initState');
super.initState();
}
@override
void didChangeDependencies() {
print('didChangeDependencies');
super.didChangeDependencies();
}
@override
void didUpdateWidget(FirstRouteWidget oldWidget) {
print('didUpdateWidget');
super.didUpdateWidget(oldWidget);
}
@override
void deactivate() {
print('deactivate');
super.deactivate();
}
@override
void dispose() {
print('[XDEBUG] - FirstRouteWidget is disposing~');
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('First Route')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: const Text('Open native page'),
onPressed: () {
print('open natve page!');
FlutterBoost.singleton
.open('native')
.then((Map<dynamic, dynamic> value) {
print(
'call me when page is finished. did recieve native route result $value');
});
},
),
RaisedButton(
child: const Text('Open FF route'),
onPressed: () {
print('open FF page!');
FlutterBoost.singleton
.open('firstFirst')
.then((Map<dynamic, dynamic> value) {
print(
'call me when page is finished. did recieve FF route result $value');
});
},
),
RaisedButton(
child: const Text('Open second route1'),
onPressed: () {
print('open second page!');
FlutterBoost.singleton
.open('second')
.then((Map<dynamic, dynamic> value) {
print(
'call me when page is finished. did recieve second route result $value');
});
},
),
RaisedButton(
child: const Text('Present second stateful route'),
onPressed: () {
print('Present second stateful page!');
FlutterBoost.singleton.open('secondStateful',
urlParams: <String, dynamic>{
'present': true
}).then((Map<dynamic, dynamic> value) {
print(
'call me when page is finished. did recieve second stateful route result $value');
});
},
),
RaisedButton(
child: const Text('Present second route'),
onPressed: () {
print('Present second page!');
FlutterBoost.singleton.open('second',
urlParams: <String, dynamic>{
'present': true
}).then((Map<dynamic, dynamic> value) {
print(
'call me when page is finished. did recieve second route result $value');
});
},
),
RaisedButton(
child: Text('Get system version by method channel:' + _systemVersion),
onPressed: () => _getPlatformVersion(),
),
],
),
),
);
}
}
/// *********************** SecondRouteWidget*/
class SecondRouteWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Second Route')),
body: Center(
child: RaisedButton(
onPressed: () {
// Navigate back to first route when tapped.
final BoostContainerSettings settings =
BoostContainer.of(context).settings;
FlutterBoost.singleton.close(
settings.uniqueId,
result: <String, dynamic>{'result': 'data from second'},
);
},
child: const Text('Go back with result!'),
),
),
);
}
}
注意點:
(1).狀態(tài)管理的處理某饰,如果細心地同學會發(fā)現露乏,上邊FirstRouteWidget和SecondRouteWidget的繼承類不同,一個是StatelessWidget(非動態(tài)的)比勉、StatefulWidget(動態(tài)的)兩種狀態(tài)管理浩聋。具體理解可以自己去查資料學習衣洁,后續(xù)也會專門講下這個坊夫。
場景:
1.原生-->>原生
這個不多說直接還是原生跳轉就行
2.原生-->>Flutter
調用PlatformRouterImp中的方法
3.Flutter-->>原生
需要Flutter代碼執(zhí)行
FlutterBoost.singleton
.open('native')
.then((Map<dynamic, dynamic> value) {
print(
'call me when page is finished. did recieve native route result $value');
});
4.Flutter-->>Flutter (走Native原生Controller容器跳轉)
FlutterBoost.singleton
.open('firstFirst')
.then((Map<dynamic, dynamic> value) {
print(
'call me when page is finished. did recieve FF route result $value');
});
5.Flutter-->>Flutter(走Flutter自己的Flutter Navigator 跳轉)
Navigator.push<dynamic>(
context,
MaterialPageRoute<dynamic>(builder: (_) => PushWidget()),
);
這個自己去做處理,建議既然使用flutter_boost就都采用flutter_boost統(tǒng)一的路有跳轉放吩。
如果想實現只打開一個Flutter的原生控制器渡紫,其他走flutter內部跳轉router邏輯惕澎,則需要自己處理集灌,或者也可以看下flutter_thrio框架复哆。
到這里基本上全部工作都可以了梯找,Flutter和iOS都會關聯上锈锤。直接運行Xcode就可以了久免。
關于分離開發(fā):
需要注意的地方如果之后考慮分開開發(fā)阎姥,Flutter同學只開發(fā)Flutter的話呼巴,需要在flutter_module/.ios/下工程配置之前iOS端的東西即可衣赶,就可以使用flutter run直接跑起來府瞄,且也能使用flutter_boost路由碘箍。做到Flutter開發(fā)Flutter敲街,原生開發(fā)原生多艇。做到分離峻黍。