該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請注明:劉小壯
iOS接入Flutter
在進(jìn)行iOS
和Flutter
的混編時澈吨,iOS
比Android
的接入方式略復(fù)雜废登,但也還好≌饣。現(xiàn)在市面上有不少接入Flutter
的方案,但大多數(shù)都是千篇一律相互抄的杉辙,沒什么意義鼓寺。
進(jìn)行Flutter
混編之前挑秉,有一些必要的文件法梯。
-
xcode_backend.sh
文件,在配置flutter
環(huán)境的時候由Flutter
工具包提供犀概。 -
xcconfig
環(huán)境變量文件立哑,在Flutter
工程中自動生成,每個工程都不一樣姻灶。
xcconfig文件
xcconfig
是Xcode
的配置文件铛绰,Flutter
在里面配置了一些基本信息和路徑,接入Flutter
前需要先將xcconfig
接入進(jìn)來产喉,否則一些路徑等信息將會出錯或找不到捂掰。
Flutter
的xcconfig
包含三個文件,Debug.xcconfig
镊叁、Release.xcconfig
、Generated.xcconfig
走触,需要將這些文件配置在下面的位置晦譬,并且按照不同環(huán)境配置不同的文件。
Project -> Info -> Development Target -> Configurations
有些比較大的工程中已經(jīng)在Configurations
中設(shè)置了xcconfig
文件互广,由于每個Target
的一種環(huán)境只能配置一個xcconfig
文件敛腌,所以可以在已有的xcconfig
文件中import
引入Generated.xcconfig
文件,并且不需要區(qū)分環(huán)境惫皱。
腳本文件
xcode_backend.sh
腳本文件用來構(gòu)建和導(dǎo)出Flutter
產(chǎn)物像樊,這是Flutter
開發(fā)包為我們默認(rèn)提供的。需要在工程Target
的Build Phases
加入一個Run Script
文件旅敷,并將下面的腳本代碼粘貼進(jìn)去生棍。需要注意的是,不要忘記前面的/bin/sh
操作媳谁,否則會導(dǎo)致權(quán)限錯誤涂滴。
/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
在xcode_backend.sh
中有三個參數(shù)類型,build
晴音、thin
柔纵、embed
,thin
沒有太大意義锤躁,其他兩個則負(fù)責(zé)構(gòu)建和導(dǎo)出搁料。
混合開發(fā)
隨后可以對Xcode
工程進(jìn)行編譯,這時候肯定會報(bào)錯的。但是不要慌張郭计,報(bào)錯后我們在工程主目錄下會發(fā)現(xiàn)一個名為Flutter
的文件夾霸琴,其中會包含兩個framework
,這個文件夾就是Flutter
的編譯產(chǎn)物拣宏,我們將這個文件夾整體拖入項(xiàng)目中即可沈贝。
這時候就可以在iOS
工程中添加Flutter
代碼了,下面是詳細(xì)步驟勋乾。
- 將
AppDelegate
的集成改為FlutterAppDelegate
宋下,并且需要遵循FlutterAppLifeCycleProvider
代理。
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
@interface AppDelegate : FlutterAppDelegate <FlutterAppLifeCycleProvider>
@end
- 創(chuàng)建一個
FlutterPluginAppLifeCycleDelegate
的實(shí)例對象辑莫,這個對象負(fù)責(zé)管理Flutter
的生命周期学歧,并從Platform
側(cè)接收AppDelegate
的事件。我直接將其聲明為一個屬性各吨,在AppDelegate
中的各個方法中枝笨,調(diào)用其方法進(jìn)行中轉(zhuǎn)操作。
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self.lifeCycleDelegate application:application willFinishLaunchingWithOptions:launchOptions];
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application {
[self.lifeCycleDelegate applicationWillResignActive:application];
}
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
[self.lifeCycleDelegate application:application openURL:url sourceApplication:sourceApplication annotation:annotation];
return YES;
}
- 隨后即可加入
Flutter
代碼揭蜒,加入的方式也很簡單横浑,直接實(shí)例化一個FlutterViewController
控制器即可,也不需要傳其他參數(shù)進(jìn)去(這里先不考慮多實(shí)例的問題)屉更。
FlutterViewController *flutterViewController = [[FlutterViewController alloc] init];
Flutter
將其看做是一個畫布徙融,實(shí)例化一個畫布上去之后,任何操作其實(shí)都是在當(dāng)前頁面完成的瑰谜。
常見錯誤
到這個步驟集成操作就已經(jīng)完成欺冀,但是很多人在集成過程中會遇到一些錯誤,下面是一些常見錯誤萨脑。
- 路徑錯誤隐轩,讀取不到
xcode_backend.sh
文件等。這是因?yàn)榄h(huán)境變量FLUTTER_ROOT
沒有獲取到渤早,FLUTTER_ROOT
配置在Generated.xcconfig
中职车,可以看一下這個文件是不是配置的有問題。 -
lipo info *** arm64
類似這樣的錯誤鹊杖,一般都是因?yàn)?code>xcode_backend.sh腳本導(dǎo)致的提鸟,可以檢查一下FLUTTER_ROOT
環(huán)境變量是否正確。 - 下面這種問題一般都是因?yàn)闄?quán)限導(dǎo)致的仅淑,可以查看
Build Phases
的腳本寫的是不是有問題称勋。
***/flutter_tools/bin/xcode_backend.sh: Permission denied
混合開發(fā)
在進(jìn)行混編過程中,Flutter
有一個很大的優(yōu)勢涯竟,就是如果Flutter
代碼出問題赡鲜,不會導(dǎo)致原生應(yīng)用的崩潰空厌。當(dāng)Flutter
代碼出現(xiàn)崩潰時,會在屏幕上顯示錯誤信息银酬。
在開發(fā)過程中經(jīng)常會涉及到網(wǎng)絡(luò)請求和持久化的問題嘲更,如果混編的話可能會涉及到寫兩套邏輯。例如網(wǎng)絡(luò)請求有一些公共參數(shù)揩瞪,或返回?cái)?shù)據(jù)的統(tǒng)一處理等赋朦,如果維護(hù)兩套邏輯的話會容易出問題。所以建議將網(wǎng)絡(luò)請求和持久化操作都交給Platform
處理李破,Flutter
側(cè)只負(fù)責(zé)向Platform
請求并拿來使用即可宠哄。
這個過程就涉及到兩端數(shù)據(jù)交互的問題,Flutter
對于混編給出了兩套方案嗤攻,MethodChannel
和EventChannel
毛嫉。從名字上來看,一個是方法調(diào)用妇菱,另一個是事件傳遞承粤。但實(shí)際開發(fā)過程中,只需要使用MethodChannel
即可完成所有需求闯团。
Flutter to Native
下面是Flutter
調(diào)用Native
的代碼辛臊,在Native
中通過FlutterMethodChannel
設(shè)置指定的回調(diào)代碼,并且在接收參數(shù)并處理房交。由Flutter
通過MethodChannel
對Native
發(fā)起調(diào)用彻舰,并傳入對應(yīng)的參數(shù)。
代碼中在Flutter
側(cè)構(gòu)建好數(shù)據(jù)模型涌萤,然后調(diào)用MethodChannel
的invokeMethod
淹遵,會觸發(fā)Native
的回調(diào)口猜。Native
拿到Flutter
傳過來的數(shù)據(jù)负溪,進(jìn)行解析并執(zhí)行播放操作,隨后會把播放的狀態(tài)碼回調(diào)給Flutter
側(cè)济炎,交互完成川抡。
import 'package:flutter/services.dart';
Future<Null> playVideo() async{
var methodChannel = MethodChannel('flutterChannelName');
Map params = {'playID' : '302998298', 'duration' : '2520', 'name' : '三生三世十里桃花'};
String result;
result = await methodChannel.invokeMethod('PlayAlbumVideo', params);
String playID = params['playID'];
String duration = params['duration'];
String name = params['name'];
showCupertinoDialog(context: context, builder: (BuildContext context){
return CupertinoAlertDialog(
title: Text(result),
content: Text('name:$name playID:$playID duration:$duration'),
actions: <Widget>[
FlatButton(
child: Text('確定'),
onPressed: (){
Navigator.pop(context);
},
)
],
);
});
}
NSString *channelName = @"flutterChannelName";
FlutterMethodChannel *methodChannel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:flutterVC];
[methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
if ([call.method isEqualToString:@"PlayAlbumVideo"]) {
NSDictionary *params = call.arguments;
VideoPlayerModel *model = [[VideoPlayerModel alloc] init];
model.playID = [params stringForKey:@"playID"];
model.duration = [params stringForKey:@"duration"];
model.name = [params stringForKey:@"name"];
NSString *playStatus = [SVHistoryPlayUtil playVideoWithModel:model
showPlayerVC:self.flutterVC];
result([NSString stringWithFormat:@"播放狀態(tài) %@", playStatus]);
}
}];
Native to Flutter
Native
調(diào)用Flutter
的代碼和Flutter
調(diào)用Native
的基本類似,只是調(diào)用和設(shè)置回調(diào)的角色不同须尚。同樣的崖堤,Flutter
由于要接收Native
的消息回調(diào),所以需要注冊一個回調(diào)耐床,由Native
發(fā)起對Flutter
的調(diào)用并傳入?yún)?shù)密幔。
Native
和Flutter
的相互調(diào)用都需要設(shè)置一個名字,每一個名字對應(yīng)一個MethodChannel
對象撩轰,每一個對象可以發(fā)起多次調(diào)用胯甩,不同調(diào)用以invokeMethod
做區(qū)分昧廷。
import 'package:flutter/services.dart';
@override
void initState() {
super.initState();
MethodChannel methodChannel = MethodChannel('nativeChannelName');
methodChannel.setMethodCallHandler(callbackHandler);
}
Future<dynamic> callbackHandler(MethodCall call) {
if(call.method == 'requestHomeData') {
String title = call.arguments['title'];
String content = call.arguments['content'];
showCupertinoDialog(context: context, builder: (BuildContext context){
return CupertinoAlertDialog(
title: Text(title),
content: Text(content),
actions: <Widget>[
FlatButton(
child: Text('確定'),
onPressed: (){
Navigator.pop(context);
},
)
],
);
});
}
}
NSString *channelName = @"nativeChannelName";
FlutterMethodChannel *methodChannel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:flutterVC];
[RequestManager requestWithURL:url success:^(NSDictionary *result) {
[methodChannel invokeMethod:@"requestHomeData" arguments:result];
}];
調(diào)試工具集
在iOS
和Android
開發(fā)中,各自的編譯器都提供了很好的調(diào)試工具集偎箫,方便進(jìn)行內(nèi)存木柬、性能、視圖等調(diào)試淹办。Flutter
也提供了調(diào)試工具和命令眉枕,下面基于VSCode
編譯器來講一下Flutter
調(diào)試,相對而言Android Studio
提供的調(diào)試功能可能會更多一些怜森。
性能調(diào)試
VSCode
支持一些簡單的命令行調(diào)試指令速挑,在程序運(yùn)行過程中,在Command Palette
命令行面板中輸入performance
塔插,并選擇Toggle Performance Overlay
命令即可梗摇。此命令有一個要求就是需要App在運(yùn)行狀態(tài)。
隨后會在界面上出現(xiàn)一個性能面板想许,這個頁面分為兩部分伶授,GPU線程和UI線程的幀率。每個部分分為三個橫線流纹,代表著不同的卡頓層級糜烹。如果是綠色則表示不會影響界面渲染,如果是紅色則有可能會影響界面的流暢性漱凝。如果出現(xiàn)紅色線條疮蹦,則表示當(dāng)前執(zhí)行的代碼需要優(yōu)化。
Dart DevTools
VSCode
為Flutter
提供了一套調(diào)試工具集-Dart DevTools
茸炒,這套工具集功能非常全愕乎,包含性能、UI壁公、熱更新感论、熱重載、log日志等很多功能紊册。
安裝Dart DevTools
后比肄,在App運(yùn)行狀態(tài)下,可以在VSCode
的右下角啟動這個工具囊陡,工具會以網(wǎng)頁的形式展現(xiàn)芳绩,并且可以控制App。
主界面
下面是Dart DevTools
的主界面撞反,我運(yùn)行的是一個界面類似于微信的App妥色。從Inspector
中可以看到頁面的視圖結(jié)構(gòu),Android Studio
也有類似的功能遏片。頁面整體是一個樹形結(jié)構(gòu)嘹害,并且選中某一個控件后鳍侣,會在右側(cè)展示出控件的變量值,例如frame
吼拥、color
等倚聚,這個功能非常實(shí)用。
我運(yùn)行的設(shè)備是Xcode
模擬器凿可,如果想切換Android
的Material Design
惑折,點(diǎn)擊上面的iOS
按鈕即可直接切換設(shè)備。剛才上面說到的查看內(nèi)存的性能面板枯跑,點(diǎn)擊iOS
按鈕旁邊的Performance Overlay
即可出現(xiàn)惨驶。
Select Widget
如果想知道在Dart DevTools
中選擇的節(jié)點(diǎn),具體對應(yīng)哪個控件敛助,可以選擇Select Widget Mode
使屏幕上被選中的控件高亮粗卜。
Debug Paint
點(diǎn)擊Debug Paint
可以讓每個控件都高亮,通過這個模式可以看到ListView
的滑動方向纳击,以及每個控件的大小及控件之間的距離续扔。
除此之外,還可以選擇Paint Baseline
使所有控件的底線高亮焕数,功能和Debug Paint
類似纱昧,不做敘述。
Memory
Dart DevTools
中提供的內(nèi)存調(diào)試工具更加直觀堡赔,可以實(shí)時顯示內(nèi)存使用情況识脆。在剛開始運(yùn)行時,我們發(fā)現(xiàn)一個內(nèi)存峰值善已,把鼠標(biāo)放上去可以看到具體的內(nèi)存使用情況灼捂。內(nèi)存會有具體分類,Used
换团、GC
等悉稠。
Dart DevTools
的內(nèi)存工具還是不夠完美,Xcode
可以選擇某段內(nèi)存啥寇,看到這塊內(nèi)存中涉及到主要堆棧調(diào)用偎球,并且點(diǎn)擊調(diào)用椚髟可以跳轉(zhuǎn)到Xcode
對應(yīng)的代碼中辑甜,而Dart DevTools
還不具備這個功能,可能和Web
的展示形式有關(guān)系袍冷。
內(nèi)存管理Flutter
使用的是GC
磷醋,回收速度可能不是很快,iOS
中的ARC
則是基于引用計(jì)數(shù)立即回收的胡诗。還有很多其他的功能邓线,這里就不一一詳細(xì)敘述了淌友,各位同學(xué)可以自己探索。
多實(shí)例
項(xiàng)目中是通過實(shí)例化FlutterViewController
控制器來顯示Flutter
界面的骇陈,整個Flutter
頁面可以理解為一個畫布震庭,通過頁面不斷的變化,改變畫布上的東西你雌。所以器联,在單實(shí)例的情況下,Flutter
頁面中間不能插入原生頁面婿崭。
這時候如果我們想在多個地方展示Flutter
頁面拨拓,而這些頁面并不是Flutter -> Flutter
的連貫跳轉(zhuǎn)形式,那怎么來實(shí)現(xiàn)這個場景呢氓栈?Google
的建議是創(chuàng)建Flutter
的多實(shí)例渣磷,并通過傳入不同的參數(shù)實(shí)例化不同的頁面。但這樣會造成很嚴(yán)重的內(nèi)存問題授瘦,所以并不能這么做醋界。
Router
如果不能真正創(chuàng)建多個實(shí)例對象,那就需要通過其他方式來實(shí)現(xiàn)多實(shí)例提完。Flutter
頁面顯示其實(shí)并不是跟著FlutterVC
走的物独,而是跟著FlutterEngine
走的。所以在創(chuàng)建一次FlutterVC
之后氯葬,就將FlutterEngine
保存下來挡篓,在其他位置創(chuàng)建FlutterVC
時直接通過FlutterEngine
的方式創(chuàng)建,并且在創(chuàng)建后進(jìn)行跳轉(zhuǎn)操作帚称。
在進(jìn)行頁面切換時官研,通過channelMethod
調(diào)用Flutter
側(cè)的路由切換代碼,并將切換后的新頁面FlutterVC
添加到Native
上闯睹。這種實(shí)現(xiàn)方式戏羽,就是通過Flutter
的Router
的方式實(shí)現(xiàn)的,下面將會介紹Router
的兩種表現(xiàn)形式楼吃,靜態(tài)路由和動態(tài)路由始花。
靜態(tài)路由
靜態(tài)路由是MaterialApp
提供的一個API
,routes
本質(zhì)上是一個Map
對象孩锡,其組成結(jié)構(gòu)是key
是調(diào)用頁面的唯一標(biāo)識符酷宵,value
就是對應(yīng)頁面的Widget
。
在定義靜態(tài)路由時躬窜,可以在創(chuàng)建Widget
時傳入?yún)?shù)浇垦,例如實(shí)例化ContactWidget
時就可以傳入對應(yīng)的參數(shù)過去。
void main() {
runApp(
MaterialApp(
home: Page2(),
routes: {
'page1': (_) => Page1(),
'page2': (_) => Page2()
},
),
);
}
class Page1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ContactWidget();
}
}
class Page2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomeScreen();
}
}
進(jìn)行頁面跳轉(zhuǎn)時荣挨,通過Navigator
進(jìn)行調(diào)用男韧,每次調(diào)用都會重新創(chuàng)建對應(yīng)的Widget
朴摊。進(jìn)行調(diào)用時pushNamed
函數(shù)會傳入一個參數(shù),這個參數(shù)就是定義Map
時對應(yīng)頁面的key
此虑。
Navigator.of(context).pushNamed('page1');
動態(tài)路由
靜態(tài)路由的方式并不是很靈活甚纲,相對而言動態(tài)路由更加靈活。動態(tài)路由不需要預(yù)先設(shè)定routes
朦前,直接調(diào)用即可贩疙。和普通push
不同的是,動態(tài)路由在push
時通過PageRouteBuilder
來構(gòu)建push
對象况既,在Builder
的構(gòu)建方法中執(zhí)行對應(yīng)的頁面跳轉(zhuǎn)操作即可这溅。
結(jié)合之前說的channelMethod
,就是在channelMethod
對應(yīng)的Callback
回調(diào)中棒仍,執(zhí)行Navigator
的push
函數(shù)悲靴,接收Native
傳遞過來的參數(shù)并構(gòu)建對應(yīng)的Widget
頁面,將Widget
返回給Builder
即可完成頁面跳轉(zhuǎn)操作莫其。所以說動態(tài)路由的方式非常靈活癞尚。
無論是通過靜態(tài)路由還是動態(tài)路由的方式創(chuàng)建,都可以通過then
函數(shù)接收新頁面返回時的返回值乱陡。
Navigator.of(context).push(PageRouteBuilder(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return ContactWidget('next page value');
}
transitionsBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(
child: child,
opacity: animation,
);
}
)).then((onValue){
print('pop的返回值 $onValue');
});
但動態(tài)路由的跳轉(zhuǎn)方式也有一些問題浇揩,會導(dǎo)致動畫失效。所以需要重寫Builder
的transitionsBuilder
函數(shù)憨颠,來自定義轉(zhuǎn)場動畫胳徽。
無論是通過靜態(tài)路由還是動態(tài)路由的方式創(chuàng)建,都會存在一些問題爽彤。由于每次都是新創(chuàng)建Widget
养盗,所以在創(chuàng)建時會有黑屏的問題。而且每次創(chuàng)建的話适篙,都會丟失當(dāng)前頁面上次的上下文狀態(tài)往核,每次進(jìn)來都是一個新頁面。
簡書由于排版的問題嚷节,閱讀體驗(yàn)并不好聂儒,布局、圖片顯示硫痰、代碼等很多問題衩婚。所以建議到我Github
上,下載Flutter編程指南 PDF
合集碍论。把所有Flutter
文章總計(jì)三篇谅猾,都寫在這個PDF
中柄慰,而且左側(cè)有目錄鳍悠,方便閱讀税娜。
下載地址:Flutter編程指南 PDF
麻煩各位大佬點(diǎn)個贊,謝謝藏研!??