Flutter移動端實戰手冊

劉小壯發表於2019-06-20
該文章屬於<簡書 — 劉小壯>原創,轉載請註明:

<簡書 — 劉小壯> https://www.jianshu.com/p/d27c1f5ee3ff


封面圖

iOS接入Flutter

在進行iOSFlutter的混編時,iOSAndroid的接入方式略複雜,但也還好。現在市面上有不少接入Flutter的方案,但大多數都是千篇一律相互抄的,沒什麼意義。

進行Flutter混編之前,有一些必要的檔案。

  1. xcode_backend.sh檔案,在配置flutter環境的時候由Flutter工具包提供。
  2. xcconfig環境變數檔案,在Flutter工程中自動生成,每個工程都不一樣。

xcconfig檔案

xcconfigXcode的配置檔案,Flutter在裡面配置了一些基本資訊和路徑,接入Flutter前需要先將xcconfig接入進來,否則一些路徑等資訊將會出錯或找不到。

Flutterxcconfig包含三個檔案,Debug.xcconfigRelease.xcconfigGenerated.xcconfig,需要將這些檔案配置在下面的位置,並且按照不同環境配置不同的檔案。

Project -> Info -> Development Target -> Configurations
複製程式碼

系統設定

有些比較大的工程中已經在Configurations中設定了xcconfig檔案,由於每個Target的一種環境只能配置一個xcconfig檔案,所以可以在已有的xcconfig檔案中import引入Generated.xcconfig檔案,並且不需要區分環境。

指令碼檔案

xcode_backend.sh指令碼檔案用來構建和匯出Flutter產物,這是Flutter開發包為我們預設提供的。需要在工程TargetBuild 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中有三個引數型別,buildthinembedthin沒有太大意義,其他兩個則負責構建和匯出。

混合開發

隨後可以對Xcode工程進行編譯,這時候肯定會報錯的。但是不要慌張,報錯後我們在工程主目錄下會發現一個名為Flutter的資料夾,其中會包含兩個framework,這個資料夾就是Flutter的編譯產物,我們將這個資料夾整體拖入專案中即可。

這時候就可以在iOS工程中新增Flutter程式碼了,下面是詳細步驟。

  1. AppDelegate的整合改為FlutterAppDelegate,並且需要遵循FlutterAppLifeCycleProvider代理。
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>

@interface AppDelegate : FlutterAppDelegate <FlutterAppLifeCycleProvider>

@end
複製程式碼
  1. 建立一個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;
}
複製程式碼
  1. 隨後即可加入Flutter程式碼,加入的方式也很簡單,直接例項化一個FlutterViewController控制器即可,也不需要傳其他引數進去(這裡先不考慮多例項的問題)。
FlutterViewController *flutterViewController = [[FlutterViewController alloc] init];
複製程式碼

Flutter將其看做是一個畫布,例項化一個畫布上去之後,任何操作其實都是在當前頁面完成的。

常見錯誤

到這個步驟整合操作就已經完成,但是很多人在整合過程中會遇到一些錯誤,下面是一些常見錯誤。

  1. 路徑錯誤,讀取不到xcode_backend.sh檔案等。這是因為環境變數FLUTTER_ROOT沒有獲取到,FLUTTER_ROOT配置在Generated.xcconfig中,可以看一下這個檔案是不是配置的有問題。
  2. lipo info *** arm64類似這樣的錯誤,一般都是因為xcode_backend.sh指令碼導致的,可以檢查一下FLUTTER_ROOT環境變數是否正確。
  3. 下面這種問題一般都是因為許可權導致的,可以檢視Build Phases的指令碼寫的是不是有問題。
***/flutter_tools/bin/xcode_backend.sh: Permission denied
複製程式碼

混合開發

在進行混編過程中,Flutter有一個很大的優勢,就是如果Flutter程式碼出問題,不會導致原生應用的崩潰。當Flutter程式碼出現崩潰時,會在螢幕上顯示錯誤資訊。

在開發過程中經常會涉及到網路請求和持久化的問題,如果混編的話可能會涉及到寫兩套邏輯。例如網路請求有一些公共引數,或返回資料的統一處理等,如果維護兩套邏輯的話會容易出問題。所以建議將網路請求和持久化操作都交給Platform處理,Flutter側只負責向Platform請求並拿來使用即可。

這個過程就涉及到兩端資料互動的問題,Flutter對於混編給出了兩套方案,MethodChannelEventChannel。從名字上來看,一個是方法呼叫,另一個是事件傳遞。但實際開發過程中,只需要使用MethodChannel即可完成所有需求。

Flutter to Native

下面是Flutter呼叫Native的程式碼,在Native中通過FlutterMethodChannel設定指定的回撥程式碼,並且在接收引數並處理。由Flutter通過MethodChannelNative發起呼叫,並傳入對應的引數。

程式碼中在Flutter側構建好資料模型,然後呼叫MethodChannelinvokeMethod,會觸發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的呼叫並傳入引數。

NativeFlutter的相互呼叫都需要設定一個名字,每一個名字對應一個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];
}];
複製程式碼

除錯工具集

iOSAndroid開發中,各自的編譯器都提供了很好的除錯工具集,方便進行記憶體、效能、檢視等除錯。Flutter也提供了除錯工具和命令,下面基於VSCode編譯器來講一下Flutter除錯,相對而言Android Studio提供的除錯功能可能會更多一些。

效能除錯

VSCode支援一些簡單的命令列除錯指令,在程式執行過程中,在Command Palette命令列皮膚中輸入performance,並選擇Toggle Performance Overlay命令即可。此命令有一個要求就是需要App在執行狀態。

效能除錯

隨後會在介面上出現一個效能皮膚,這個頁面分為兩部分,GPU執行緒和UI執行緒的幀率。每個部分分為三個橫線,代表著不同的卡頓層級。如果是綠色則表示不會影響介面渲染,如果是紅色則有可能會影響介面的流暢性。如果出現紅色線條,則表示當前執行的程式碼需要優化。

Dart DevTools

VSCodeFlutter提供了一套除錯工具集-Dart DevTools,這套工具集功能非常全,包含效能、UI、熱更新、熱過載、log日誌等很多功能。

安裝Dart DevTools後,在App執行狀態下,可以在VSCode的右下角啟動這個工具,工具會以網頁的形式展現,並且可以控制App。

主介面

下面是Dart DevTools的主介面,我執行的是一個介面類似於微信的App。從Inspector中可以看到頁面的檢視結構,Android Studio也有類似的功能。頁面整體是一個樹形結構,並且選中某一個控制元件後,會在右側展示出控制元件的變數值,例如framecolor等,這個功能非常實用。

Dart DevTools

我執行的裝置是Xcode模擬器,如果想切換AndroidMaterial Design,點選上面的iOS按鈕即可直接切換裝置。剛才上面說到的檢視記憶體的效能皮膚,點選iOS按鈕旁邊的Performance Overlay即可出現。

Select Widget

如果想知道在Dart DevTools中選擇的節點,具體對應哪個控制元件,可以選擇Select Widget Mode使螢幕上被選中的控制元件高亮。

Select Widget Mode

Debug Paint

點選Debug Paint可以讓每個控制元件都高亮,通過這個模式可以看到ListView的滑動方向,以及每個控制元件的大小及控制元件之間的距離。

Debug Paint

除此之外,還可以選擇Paint Baseline使所有控制元件的底線高亮,功能和Debug Paint類似,不做敘述。

Memory

Dart DevTools中提供的記憶體除錯工具更加直觀,可以實時顯示記憶體使用情況。在剛開始執行時,我們發現一個記憶體峰值,把滑鼠放上去可以看到具體的記憶體使用情況。記憶體會有具體分類,UsedGC等。

Memory

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上。這種實現方式,就是通過FlutterRouter的方式實現的,下面將會介紹Router的兩種表現形式,靜態路由和動態路由。

靜態路由

靜態路由是MaterialApp提供的一個APIroutes本質上是一個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回撥中,執行Navigatorpush函式,接收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');
});
複製程式碼

但動態路由的跳轉方式也有一些問題,會導致動畫失效。所以需要重寫BuildertransitionsBuilder函式,來自定義轉場動畫。

無論是通過靜態路由還是動態路由的方式建立,都會存在一些問題。由於每次都是新建立Widget,所以在建立時會有黑屏的問題。而且每次建立的話,都會丟失當前頁面上次的上下文狀態,每次進來都是一個新頁面。


簡書由於排版的問題,閱讀體驗並不好,佈局、圖片顯示、程式碼等很多問題。所以建議到我Github上,下載Flutter程式設計指南 PDF合集。把所有Flutter文章總計三篇,都寫在這個PDF中,而且左側有目錄,方便閱讀。

Flutter程式設計指南

下載地址:Flutter程式設計指南 PDF 麻煩各位大佬點個贊,謝謝!?

相關文章