該文章屬於<簡書 — 劉小壯>原創,轉載請註明:
<簡書 — 劉小壯> https://www.jianshu.com/p/d27c1f5ee3ff
iOS接入Flutter
在進行iOS
和Flutter
的混編時,iOS
比Android
的接入方式略複雜,但也還好。現在市面上有不少接入Flutter
的方案,但大多數都是千篇一律相互抄的,沒什麼意義。
進行Flutter
混編之前,有一些必要的檔案。
xcode_backend.sh
檔案,在配置flutter
環境的時候由Flutter
工具包提供。xcconfig
環境變數檔案,在Flutter
工程中自動生成,每個工程都不一樣。
xcconfig檔案
xcconfig
是Xcode
的配置檔案,Flutter
在裡面配置了一些基本資訊和路徑,接入Flutter
前需要先將xcconfig
接入進來,否則一些路徑等資訊將會出錯或找不到。
Flutter
的xcconfig
包含三個檔案,Debug.xcconfig
、Release.xcconfig
、Generated.xcconfig
,需要將這些檔案配置在下面的位置,並且按照不同環境配置不同的檔案。
Project -> Info -> Development Target -> Configurations
複製程式碼
有些比較大的工程中已經在Configurations
中設定了xcconfig
檔案,由於每個Target
的一種環境只能配置一個xcconfig
檔案,所以可以在已有的xcconfig
檔案中import
引入Generated.xcconfig
檔案,並且不需要區分環境。
指令碼檔案
xcode_backend.sh
指令碼檔案用來構建和匯出Flutter
產物,這是Flutter
開發包為我們預設提供的。需要在工程Target
的Build Phases
加入一個Run Script
檔案,並將下面的指令碼程式碼貼上進去。需要注意的是,不要忘記前面的/bin/sh
操作,否則會導致許可權錯誤。
/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
中有三個引數型別,build
、thin
、embed
,thin
沒有太大意義,其他兩個則負責構建和匯出。
混合開發
隨後可以對Xcode
工程進行編譯,這時候肯定會報錯的。但是不要慌張,報錯後我們在工程主目錄下會發現一個名為Flutter
的資料夾,其中會包含兩個framework
,這個資料夾就是Flutter
的編譯產物,我們將這個資料夾整體拖入專案中即可。
這時候就可以在iOS
工程中新增Flutter
程式碼了,下面是詳細步驟。
- 將
AppDelegate
的整合改為FlutterAppDelegate
,並且需要遵循FlutterAppLifeCycleProvider
代理。
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
@interface AppDelegate : FlutterAppDelegate <FlutterAppLifeCycleProvider>
@end
複製程式碼
- 建立一個
FlutterPluginAppLifeCycleDelegate
的例項物件,這個物件負責管理Flutter
的生命週期,並從Platform
側接收AppDelegate
的事件。我直接將其宣告為一個屬性,在AppDelegate
中的各個方法中,呼叫其方法進行中轉操作。
- (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
程式碼,加入的方式也很簡單,直接例項化一個FlutterViewController
控制器即可,也不需要傳其他引數進去(這裡先不考慮多例項的問題)。
FlutterViewController *flutterViewController = [[FlutterViewController alloc] init];
複製程式碼
Flutter
將其看做是一個畫布,例項化一個畫布上去之後,任何操作其實都是在當前頁面完成的。
常見錯誤
到這個步驟整合操作就已經完成,但是很多人在整合過程中會遇到一些錯誤,下面是一些常見錯誤。
- 路徑錯誤,讀取不到
xcode_backend.sh
檔案等。這是因為環境變數FLUTTER_ROOT
沒有獲取到,FLUTTER_ROOT
配置在Generated.xcconfig
中,可以看一下這個檔案是不是配置的有問題。 lipo info *** arm64
類似這樣的錯誤,一般都是因為xcode_backend.sh
指令碼導致的,可以檢查一下FLUTTER_ROOT
環境變數是否正確。- 下面這種問題一般都是因為許可權導致的,可以檢視
Build Phases
的指令碼寫的是不是有問題。
***/flutter_tools/bin/xcode_backend.sh: Permission denied
複製程式碼
混合開發
在進行混編過程中,Flutter
有一個很大的優勢,就是如果Flutter
程式碼出問題,不會導致原生應用的崩潰。當Flutter
程式碼出現崩潰時,會在螢幕上顯示錯誤資訊。
在開發過程中經常會涉及到網路請求和持久化的問題,如果混編的話可能會涉及到寫兩套邏輯。例如網路請求有一些公共引數,或返回資料的統一處理等,如果維護兩套邏輯的話會容易出問題。所以建議將網路請求和持久化操作都交給Platform
處理,Flutter
側只負責向Platform
請求並拿來使用即可。
這個過程就涉及到兩端資料互動的問題,Flutter
對於混編給出了兩套方案,MethodChannel
和EventChannel
。從名字上來看,一個是方法呼叫,另一個是事件傳遞。但實際開發過程中,只需要使用MethodChannel
即可完成所有需求。
Flutter to Native
下面是Flutter
呼叫Native
的程式碼,在Native
中通過FlutterMethodChannel
設定指定的回撥程式碼,並且在接收引數並處理。由Flutter
通過MethodChannel
對Native
發起呼叫,並傳入對應的引數。
程式碼中在Flutter
側構建好資料模型,然後呼叫MethodChannel
的invokeMethod
,會觸發Native
的回撥。Native
拿到Flutter
傳過來的資料,進行解析並執行播放操作,隨後會把播放的狀態碼回撥給Flutter
側,互動完成。
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:@"播放狀態 %@", playStatus]);
}
}];
複製程式碼
Native to Flutter
Native
呼叫Flutter
的程式碼和Flutter
呼叫Native
的基本類似,只是呼叫和設定回撥的角色不同。同樣的,Flutter
由於要接收Native
的訊息回撥,所以需要註冊一個回撥,由Native
發起對Flutter
的呼叫並傳入引數。
Native
和Flutter
的相互呼叫都需要設定一個名字,每一個名字對應一個MethodChannel
物件,每一個物件可以發起多次呼叫,不同呼叫以invokeMethod
做區分。
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];
}];
複製程式碼
除錯工具集
在iOS
和Android
開發中,各自的編譯器都提供了很好的除錯工具集,方便進行記憶體、效能、檢視等除錯。Flutter
也提供了除錯工具和命令,下面基於VSCode
編譯器來講一下Flutter
除錯,相對而言Android Studio
提供的除錯功能可能會更多一些。
效能除錯
VSCode
支援一些簡單的命令列除錯指令,在程式執行過程中,在Command Palette
命令列皮膚中輸入performance
,並選擇Toggle Performance Overlay
命令即可。此命令有一個要求就是需要App在執行狀態。
隨後會在介面上出現一個效能皮膚,這個頁面分為兩部分,GPU執行緒和UI執行緒的幀率。每個部分分為三個橫線,代表著不同的卡頓層級。如果是綠色則表示不會影響介面渲染,如果是紅色則有可能會影響介面的流暢性。如果出現紅色線條,則表示當前執行的程式碼需要優化。
Dart DevTools
VSCode
為Flutter
提供了一套除錯工具集-Dart DevTools
,這套工具集功能非常全,包含效能、UI、熱更新、熱過載、log日誌等很多功能。
安裝Dart DevTools
後,在App執行狀態下,可以在VSCode
的右下角啟動這個工具,工具會以網頁的形式展現,並且可以控制App。
主介面
下面是Dart DevTools
的主介面,我執行的是一個介面類似於微信的App。從Inspector
中可以看到頁面的檢視結構,Android Studio
也有類似的功能。頁面整體是一個樹形結構,並且選中某一個控制元件後,會在右側展示出控制元件的變數值,例如frame
、color
等,這個功能非常實用。
我執行的裝置是Xcode
模擬器,如果想切換Android
的Material Design
,點選上面的iOS
按鈕即可直接切換裝置。剛才上面說到的檢視記憶體的效能皮膚,點選iOS
按鈕旁邊的Performance Overlay
即可出現。
Select Widget
如果想知道在Dart DevTools
中選擇的節點,具體對應哪個控制元件,可以選擇Select Widget Mode
使螢幕上被選中的控制元件高亮。
Debug Paint
點選Debug Paint
可以讓每個控制元件都高亮,通過這個模式可以看到ListView
的滑動方向,以及每個控制元件的大小及控制元件之間的距離。
除此之外,還可以選擇Paint Baseline
使所有控制元件的底線高亮,功能和Debug Paint
類似,不做敘述。
Memory
Dart DevTools
中提供的記憶體除錯工具更加直觀,可以實時顯示記憶體使用情況。在剛開始執行時,我們發現一個記憶體峰值,把滑鼠放上去可以看到具體的記憶體使用情況。記憶體會有具體分類,Used
、GC
等。
Dart DevTools
的記憶體工具還是不夠完美,Xcode
可以選擇某段記憶體,看到這塊記憶體中涉及到主要堆疊呼叫,並且點選呼叫棧可以跳轉到Xcode
對應的程式碼中,而Dart DevTools
還不具備這個功能,可能和Web
的展示形式有關係。
記憶體管理Flutter
使用的是GC
,回收速度可能不是很快,iOS
中的ARC
則是基於引用計數立即回收的。還有很多其他的功能,這裡就不一一詳細敘述了,各位同學可以自己探索。
多例項
專案中是通過例項化FlutterViewController
控制器來顯示Flutter
介面的,整個Flutter
頁面可以理解為一個畫布,通過頁面不斷的變化,改變畫布上的東西。所以,在單例項的情況下,Flutter
頁面中間不能插入原生頁面。
這時候如果我們想在多個地方展示Flutter
頁面,而這些頁面並不是Flutter -> Flutter
的連貫跳轉形式,那怎麼來實現這個場景呢?Google
的建議是建立Flutter
的多例項,並通過傳入不同的引數例項化不同的頁面。但這樣會造成很嚴重的記憶體問題,所以並不能這麼做。
Router
如果不能真正建立多個例項物件,那就需要通過其他方式來實現多例項。Flutter
頁面顯示其實並不是跟著FlutterVC
走的,而是跟著FlutterEngine
走的。所以在建立一次FlutterVC
之後,就將FlutterEngine
儲存下來,在其他位置建立FlutterVC
時直接通過FlutterEngine
的方式建立,並且在建立後進行跳轉操作。
在進行頁面切換時,通過channelMethod
呼叫Flutter
側的路由切換程式碼,並將切換後的新頁面FlutterVC
新增到Native
上。這種實現方式,就是通過Flutter
的Router
的方式實現的,下面將會介紹Router
的兩種表現形式,靜態路由和動態路由。
靜態路由
靜態路由是MaterialApp
提供的一個API
,routes
本質上是一個Map
物件,其組成結構是key
是呼叫頁面的唯一識別符號,value
就是對應頁面的Widget
。
在定義靜態路由時,可以在建立Widget
時傳入引數,例如例項化ContactWidget
時就可以傳入對應的引數過去。
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();
}
}
複製程式碼
進行頁面跳轉時,通過Navigator
進行呼叫,每次呼叫都會重新建立對應的Widget
。進行呼叫時pushNamed
函式會傳入一個引數,這個引數就是定義Map
時對應頁面的key
。
Navigator.of(context).pushNamed('page1');
複製程式碼
動態路由
靜態路由的方式並不是很靈活,相對而言動態路由更加靈活。動態路由不需要預先設定routes
,直接呼叫即可。和普通push
不同的是,動態路由在push
時通過PageRouteBuilder
來構建push
物件,在Builder
的構建方法中執行對應的頁面跳轉操作即可。
結合之前說的channelMethod
,就是在channelMethod
對應的Callback
回撥中,執行Navigator
的push
函式,接收Native
傳遞過來的引數並構建對應的Widget
頁面,將Widget
返回給Builder
即可完成頁面跳轉操作。所以說動態路由的方式非常靈活。
無論是通過靜態路由還是動態路由的方式建立,都可以通過then
函式接收新頁面返回時的返回值。
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');
});
複製程式碼
但動態路由的跳轉方式也有一些問題,會導致動畫失效。所以需要重寫Builder
的transitionsBuilder
函式,來自定義轉場動畫。
無論是通過靜態路由還是動態路由的方式建立,都會存在一些問題。由於每次都是新建立Widget
,所以在建立時會有黑屏的問題。而且每次建立的話,都會丟失當前頁面上次的上下文狀態,每次進來都是一個新頁面。
簡書由於排版的問題,閱讀體驗並不好,佈局、圖片顯示、程式碼等很多問題。所以建議到我Github
上,下載Flutter程式設計指南 PDF
合集。把所有Flutter
文章總計三篇,都寫在這個PDF
中,而且左側有目錄,方便閱讀。
下載地址:Flutter程式設計指南 PDF 麻煩各位大佬點個贊,謝謝!?