Flutter移動端實戰手冊

該文章屬於<簡書 — 劉小壯>原創,轉載請註明:

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


封面圖

iOS接入Flutter

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

進行Flutter混編以前,有一些必要的文件。微信

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

xcconfig文件

xcconfigXcode的配置文件,Flutter在裏面配置了一些基本信息和路徑,接入Flutter前須要先將xcconfig接入進來,不然一些路徑等信息將會出錯或找不到。網絡

Flutterxcconfig包含三個文件,Debug.xcconfigRelease.xcconfigGenerated.xcconfig,須要將這些文件配置在下面的位置,而且按照不一樣環境配置不一樣的文件。app

Project -> Info -> Development Target -> Configurations

系統設置

有些比較大的工程中已經在Configurations中設置了xcconfig文件,因爲每一個Target的一種環境只能配置一個xcconfig文件,因此能夠在已有的xcconfig文件中import引入Generated.xcconfig文件,而且不須要區分環境。less

腳本文件

xcode_backend.sh腳本文件用來構建和導出Flutter產物,這是Flutter開發包爲咱們默認提供的。須要在工程TargetBuild Phases加入一個Run Script文件,並將下面的腳本代碼粘貼進去。須要注意的是,不要忘記前面的/bin/sh操做,不然會致使權限錯誤。async

/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沒有太大意義,其餘兩個則負責構建和導出。ide

混合開發

隨後能夠對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,因此在建立時會有黑屏的問題。並且每次建立的話,都會丟失當前頁面上次的上下文狀態,每次進來都是一個新頁面。

相關文章
相關標籤/搜索