移動端開發新趨勢Flutter

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

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


封面圖

介紹

FlutterGoogle開發的新一代跨平臺方案,Flutter可以實現寫一份程式碼同時執行在iOS和Android裝置上,並且提供很好的效能體驗。Flutter使用Dart作為開發語言,這是一門簡潔、強型別的程式語言。Flutter對於iOS和Android裝置,提供了兩套視覺庫,可以針對不同的平臺有不同的展示效果。

Flutter原本是為了解決Web開發中的一些問題,而開發的一套精簡版Web框架,擁有獨立的渲染引擎和開發語言,但後來逐漸演變為移動端開發框架。正是由於Dart當初的定位是為了替代JS成為Web框架,所以Dart的語法更接近於JS語法。例如定義物件構建方法,以及例項化物件的方式等。

Google剛推出Flutter時,其發展很緩慢,終於在18年釋出第一個Bate版之後迎來了爆發性增長,釋出第一個Release版時增長速度更快。可以從Github上Star資料看出來這個增長的過程。在19年最新的Flutter 1.2版本中,已經開放Web支援的Beta版。

增長趨勢

目前已經有不少大型專案接入Flutter,阿里的鹹魚、頭條的抖音、騰訊的NOW直播,都將Flutter當做應用程式的開發語言。除此之外,還有一些其他中小型公司也在做。

整體架構

Flutter可以理解為開發SDK或者工具包,其通過Dart作為開發語言,並且提供MaterialCupertino兩套視覺控制元件,檢視或其他和檢視相關的類,都以Widget的形式表現。Flutter有自己的渲染引擎,並不依賴原生平臺的渲染。Flutter還包含一個用C++實現的Engine,渲染也是包含在其中的。

Flutter整體架構

Engine

Flutter是一套全新的跨平臺方案,Flutter並不像React Native那樣,依賴原生應用的渲染,而是自己有自己的渲染引擎,並使用Dart當做Flutter的開發語言。Flutter整體框架分為兩層,底層是通過C++實現的引擎部分,SkiaFlutter的渲染引擎,負責跨平臺的圖形渲染。Dart作為Flutter的開發語言,在C++引擎上層是DartFramework

Flutter不僅僅提供了一套視覺庫,在Flutter整體框架中包含各個層級階段的庫。例如實現一個遊戲功能,上面一些遊戲控制元件可以用上層視覺庫,底層遊戲可以直接基於Flutter的底層庫進行開發,而不需要呼叫原生應用的底層庫。Flutter的底層庫是基於Open GL實現的,所以Open GL可以做的Flutter都可以。

視覺庫

在上層Framework中包含兩套視覺庫,符合Android風格的Material,和符合iOS風格的Cupertino。也可以在此基礎上,封裝自己風格的系統元件。Cupertino是一套iOS風格的視覺庫,包含iOS的導航欄、buttonalertView等。

Flutter對不同硬體平臺有不同的相容,例如同樣的Material程式碼執行在iOS和Android不同平臺上,有一些平臺特有的顯示和互動,Flutter依然對其進行了區分適配。例如滑動ScrollView時,iOS平臺是有回彈效果的,而Android平臺則是阻尼效果。例如iOS的導航欄標題是居中的,Android導航欄標題是向左的,等等。這些Flutter都做了區分相容。

除了Flutter為我們做的一些適配外,有一些控制元件是需要我們自己做適配的,例如AlertView,在Android和iOS兩個平臺下的表現就是不同的。這些iOS特性的控制元件都定義在Cupertino中,所以建議在進行App開發時,對一些控制元件進行上層封裝。

例如AlertView則對其進行一個二次封裝,控制元件內部進行裝置判斷並選擇不同的視覺庫,這樣可以保證各個平臺的效果。

iOS風格

Android風格

雖然Flutter對於iOS和Android兩個平臺,開發有cupertinomaterial兩個視覺庫,但實際開發過程中的選擇,應該使用material當做視覺庫。因為Flutter對iOS的支援並不是很好,主要對Android平臺支援比較好,material中的UI控制元件要比cupertino多好幾倍。

Dart

DartGoogle在2011年推出的一款應用於Web開發的程式語言,Dart剛推出的時候,定位是替代JS做前端開發,後來逐步擴充套件到移動端和服務端。

Dart語言

DartFlutter的開發語言,Flutter必須遵循Dart的語言特性。在此基礎上,也會有自己的東西,例如Flutter的上層Framework,自己的渲染引擎等。可以說,Dart只是Flutter的一部分。

Dart是強型別的,對定義的變數不需要宣告其型別,Flutter會對其進行型別推導。如果不想使用型別推導,也可以自己宣告指定的型別。

Hot Reload

Flutter支援亞秒級熱過載,Android StudioVSCode都支援Hot Reload的特性。但需要區分的是,熱過載和熱更新是不同的兩個概念,熱過載是在執行除錯狀態下,將新程式碼直接更新到執行中的二進位制。而熱更新是在上線後,通過Runtime或其他方式,改變現有執行邏輯。

AOT、JIT

Flutter支援AOT(Ahead of time)和JIT(Just in time)兩種編譯模式,JIT模式支援在執行過程中進行Hot Reload。重新整理過程是一個增量的過程,由系統對本次和上次的程式碼做一次snapshot,將新的程式碼注入到DartVM中進行重新整理。但有時會不能進行Hot Reload,此時進行一次全量的Hot Reload即可。

AOT模式則是在執行前預先編譯好,這樣在每次執行過程中就不需要進行分析、編譯,此模式的執行速度是最快的。Flutter同時採用了兩種方案,在開發階段採用JIT模式進行開發,在release階段採用AOT模式,將程式碼打包為二進位制進行釋出。

在開發原生應用時,每次修改程式碼後都需要重新編譯,並且執行到硬體裝置上。由於Flutter支援Hot Reload,可以進行熱過載,對專案的開發效率有很大的提升。

由於Flutter實現機制支援JIT的原因,理論上來說是支援熱更新以及伺服器下發程式碼的。可以從伺服器。但是由於這樣會使效能變差,而且還有稽核的問題,所以Flutter並沒有採用這種方案。

實現原理

Flutter的熱過載是基於State的,也就是我們在程式碼中經常出現的setState方法,通過這個來修改後,會執行相應的build方法,這就是熱過載的基本過程。

Flutterhot reload的實現原始碼在下面路徑中,在此路徑中包含run_cold.dartrun_hot.dart兩個檔案,前者負責冷啟動,後者負責熱過載。

~/flutter/packages/flutter_tools/lib/src/run_hot.dart
複製程式碼

熱過載的程式碼實現在run_hot.dart檔案中,有HotRunner來負責具體程式碼執行。當Flutter進行熱過載時,會呼叫restart函式,函式內部會傳入一個fullRestartbool型別變數。熱過載分為全量和非全量,fullRestart引數就是表示是否全量。以非全量熱過載為例,函式的fullRestart會傳入false,根據傳入false引數,下面是部分核心程式碼。

Future<OperationResult> restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) async {
    if (fullRestart) {
        // .....
    } else {
        final bool reloadOnTopOfSnapshot = _runningFromSnapshot;
        final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing';
        final Status status = logger.startProgress(
            '$progressPrefix hot reload...',
            progressId: 'hot.reload'
        );
        OperationResult result;
        try {
            result = await _reloadSources(pause: pauseAfterRestart, reason: reason);
        } finally {
            status.cancel();
        }
    }
}
複製程式碼

呼叫restart函式後,內部會呼叫_reloadSources函式,去執行內部邏輯。下面是大概邏輯執行流程。

執行流程

_reloadSources函式內部,會呼叫_updateDevFS函式,函式內部會掃描修改的檔案,並將檔案修改前後進行對比,隨後會將被改動的程式碼生成一個kernel files檔案。

隨後會通過HTTP Server將生成的kernel files檔案傳送給Dart VM虛擬機器,虛擬機器拿到kernel檔案後會呼叫_reloadSources函式進行資源過載,將kernel檔案注入正在執行的Dart VM中。當資源過載完成後,會呼叫RPC介面觸發Widgets的重繪。

跨平臺方案對比

現在市面上RN、Weex的技術方案基本一樣,所以這裡就以RN來代表類似的跨平臺方案。Flutter是基於GPU進行渲染的,而RN則將渲染交給原生平臺,而自己只是負責通過JSCore將檢視組織起來,並處理業務邏輯。所以在渲染效果和效能這塊,Flutter的效能比RN要強很多。

跨平臺方案一般都需要對各個平臺進行平臺適配,也就是建立各自平臺的適配層,RN的平臺適配層要比Flutter要大很多。因為從技術實現來說,RN是通過JSCore引擎進行原生程式碼呼叫的,和原生程式碼互動很多,所以需要更多的適配。而Flutter則只需要對各自平臺獨有的特性進行適配即可,例如呼叫系統相簿、貼上板等。

Flutter技術實現是基於更底層實現的,對平臺依賴度不是很高,相對來說,RN對平臺的依賴度是很高的。所以RN未來的技術升級,包括擴充套件之類的,都會受到很大的限制。而Flutter未來的潛力將會很大,可以做很多技術改進。

Widget

Flutter中將顯示以及和顯示相關的部分,都統一定義為widget,下面列舉一些widget包含的型別:

  1. 用於顯示的檢視,例如ListViewTextContainer等。
  2. 用來操作檢視,例如Transform等動畫相關。
  3. 檢視佈局相關,例如CenterExpandedColumn等。

Flutter中,所有的檢視都是由Widget組成,LabelAppBarViewController等。在Flutter的設計中,組合的優先順序要大於繼承,整體檢視類結構繼承層級很淺但單層很多類。如果想定製或封裝一些控制元件,也應該以組合為主,而不是繼承。

在iOS開發中,我也經常採用這種設計方案,組合大於繼承。因為如果繼承層級過多的話,一個是不便於閱讀程式碼,還有就是不好維護程式碼。例如底層需要改一個通用的樣式,但這個類的繼承層級比較複雜,這樣改動的話影響範圍就比較大,會將一些不需要改的也改掉,這時候就會發現繼承很雞肋。但在iOS中有Category的概念,這也是一種組合的方式,可以通過將一些公共的東西放在Category中,使繼承的方便性和組合的靈活性達到一個平衡。

Flutter中並沒有單獨的佈局檔案,例如iOS的XIB這種,程式碼都在Widget中定義。和UIView的區別在於,Widget只是負責描述檢視,並不參與檢視的渲染。UIView也是負責描述檢視,而UIViewlayer則負責渲染操作,這是二者的區別。

Widget結構

瞭解Widget

在應用程式啟動時,main方法接收一個Widget當做主頁面,所以任何一個Widget都可以當做根檢視。一般都是傳一個MaterialApp,也可以傳一個Container當做根檢視,這都是被允許的。

Flutter應用中,和介面顯示及使用者互動的物件都是由Widget構成的,例如檢視、動畫、手勢等。Widget分為StatelessWidgetStatefulWidget兩種,分別是無狀態和有狀態的Widget

StatefulWidget本質上也是無狀態的,其通過State來處理Widget的狀態,以達到有狀態,State出現在整個StatefulWidget的生命週期中。

當構建一個Widget時,可以通過其build獲得構建流程,在構建流程中可以加入自己的定製操作,例如對其設定title或檢視等。

return Scaffold(
  appBar: AppBar(
    title: Text('ListView Demo'),
  ),
  body: ListView.builder(
    itemCount: dataList.length,
    itemBuilder: (BuildContext context, int index) {
      return Text(dataList[index]);
    },
  ),
);
複製程式碼

有些Widget在構建時,也提供一些引數來幫助構建,例如構建一個ListView時,會將index返回給build方法,來區別構建的Cell,以及構建的上下文context

itemBuilder: (BuildContext context, int index) {
  return Text(dataList[index]);
}
複製程式碼

StatelessWidget

StatelessWidget是一種靜態Widget,即建立後自身就不能再進行改變。在建立一個StatelessWidget後,需要重寫build函式。每個靜態Widget都會有一個build函式,在建立檢視物件時會呼叫此方法。同樣的,此函式也接收一個Widget型別的返回值。

class RectangleWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center (
        // UI Code
    );
  }
}
複製程式碼

StatefulWidget

Widget本質上是不可被改變的,但StatefulWidget將狀態拆分到State中去管理,當資料發生改變時由State去處理檢視的改變。

下面是建立一個動態Widget,當建立一個動態Widget需要配合一個State,並且需要重寫createState方法。重寫此函式後,指定一個Widget對應的State並初始化。

下面例子中,在StatefulWidget的父類中包含一個Key型別的key變數,這是無論靜態Widget還是動態Widget都具備的引數。在動態Widget中定義了自己的成員變數title,並在自定義的初始化方法中傳入,通過下面DynamicWidget類的構造方法,並不需要在內部手動進行title的賦值,title即為傳入的值,是由系統完成的。

class DynamicWidget extends StatefulWidget {
  DynamicWidget({Key key, this.title}) : super (key : key);
  final String title;

  @override
  DynamicWidgetState createState() => new DynamicWidgetState();
}
複製程式碼

由於上面動態Widget定義了初始化方法,在呼叫動態Widget時可以直接用自定義初始化方法即可。

DynamicWidget(key: 'key', title: 'title');
複製程式碼

State

StatefulWidget的改變是由State來完成的,State中需要重寫build方法,在build中進行檢視組織。StatefulWidget是一種響應式檢視改變的方式,資料來源和檢視產生繫結關係,由資料來源驅動檢視的改變。

改變StatefulWidget的資料來源時,需要呼叫setState方法,並將資料來源改變的操作寫在裡面。使用動態Widget後,是不需要我們手動去重新整理檢視的。系統在setState方法呼叫後,會重新呼叫對應Widgetbuild方法,重新繪製某個Widget

下面的程式碼示例中新增了一個float按鈕,並給按鈕設定了一個回撥函式_onPressAction,這樣在每次觸發按鈕事件時都會呼叫此函式。counter是一個整型變數並和Text相關聯,當counter的值在setState方法中改變時,Text Widget也會跟著變化。

class DynamicWidgetState extends State<DynamicWidget> {
  int counter = 0;
  void _onPressAction() {
    setState(() {
      counter++;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: Center(
        child: Text('Button tapped $_counter.')
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _onPressAction,
        tooltip: 'Increment',
        child: Icon(Icons.add)
      )
    );
  }  
}
複製程式碼

主要Widget

在iOS中有UINavigationController的概念,其並不負責顯示,而是負責控制各個頁面的跳轉操作。在Flutter中可以將MaterialApp理解為iOS的導航控制器,其包含一個navigationBar以及導航棧,這和iOS是一樣的。

在iOS中除了用來顯示的檢視外,檢視還有對應的UIViewController。在Flutter中並沒有專門用來管理檢視並且和View一對一的類,但從顯示的角度來說,有類似的類Scaffold,其包含控制器的appBar,也可以通過body設定一個widget當做其檢視。

theme

themeFlutter提供的介面風格API,MaterialApp提供有theme屬性,可以在MaterialApp中設定全域性樣式,這樣可以統一整個應用的風格。

new MaterialApp(
  title: title,
  theme: new ThemeData(
    brightness: Brightness.dark,
    primaryColor: Colors.lightBlue[800],
    accentColor: Colors.cyan[600],
  )
);
複製程式碼

如果不想使用系統預設主題,可以將對應的控制元件或試圖用Theme包起來,並將Theme當做Widget賦值給其他Widget

new Theme(
  data: new ThemeData(
    accentColor: Colors.yellow,
  ),
  child: new FloatingActionButton(
    onPressed: () {},
    child: new Icon(Icons.add),
  ),
);
複製程式碼

有時MaterialApp設定的統一風格,並不能滿足某個Widget的要求,可能還需要有其他的外觀變化,可以通過Theme.of傳入當前的BuildContext,來對theme進行擴充套件。

Flutter會根據傳入的context,順著Widget樹查詢最近的Theme,並對Theme複製一份防止影響原有的Theme,並對其進行擴充套件。

new Theme(
  data: Theme.of(context).copyWith(accentColor: Colors.yellow),
  child: new FloatingActionButton(
    onPressed: null,
    child: new Icon(Icons.add),
  ),
);
複製程式碼

網路請求

Flutter中可以通過asyncawait組合使用,進行網路請求。Flutter中的網路請求大體有三種:

  1. 系統自帶的HttpClient網路請求,缺點是程式碼量相對而言比較多,而且對post請求支援不是很好。
  2. 三方庫http.dart,請求簡單。
  3. 三方庫dio,請求簡單。

http網路庫

http網路庫定義在http.dart中,內部程式碼定義很全,包括HttpStatusHttpHeadersCookie等很多基礎資訊,有助於我們瞭解http請求協議。

因為是三方庫,所以需要在pubspec.yaml中加入下面的引用。

http: '>=0.11.3+12'
複製程式碼

下面是http.dart的請求示例程式碼,可以看到請求很簡單,真正的請求程式碼其實就兩行。生成一個Client請求物件,呼叫client例項的get方法(如果是post則呼叫post方法),並用Response物件去接收請求結果即可。

通過async修飾發起請求的方法,表示這是一個非同步操作,並在請求程式碼的前面加入await,修飾這裡的程式碼需要等待資料返回,需要過一段時間後再處理。

請求回來的資料預設是json字串,需要對其進行decode並解析為資料物件才可以使用,這裡使用系統自帶的convert庫進行解析,並解析為陣列。

import 'package:http/http.dart' as http;

class RequestDemoState extends State<MyHomePage> {
  List dataList = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  // 發起網路請求
  loadData() async{
    String requestURL = 'https://jsonplaceholder.typicode.com/posts';
    Client client = Client();
    Response response = await client.get(requestURL);

    String jsonString = response.body;
    setState(() {
      // 資料解析
      dataList = json.decode(jsonString);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title)
      ),
      body: ListView.builder(
        itemCount: dataList.length,
        itemBuilder: (BuildContext context, int index) {
          return Text(dataList[index]['title']);
        },
      ),
    );
  }
}
複製程式碼

在呼叫Client進行post資料請求時,需要傳入一個字典進去,Client會通過將字典當做post的from表單。

 void requestData() async {
    var params = Map<String, String>();
    params["username"] = "lxz";
    params["password"] = "123456";

    var client = http.Client();
    var response = await client.post(url_post, body: params);
    _content = response.body;
}
複製程式碼

dio網路庫

dio庫的呼叫方式和http庫類似,這裡不過多介紹。dio庫相對於http庫強大的在於,dio庫提供了更好的Cookie管理、檔案的上傳下載、fromData表單等處理。所以,如果對網路庫需求比較複雜的話,還是建議使用dio

// 引入外部依賴
dio: ^1.0.9
複製程式碼

資料解析

convert

系統自帶有convert解析庫,在使用時直接import即可。convert類似於iOS自帶的JSON解析類NSJSONSerialization,可以直接將json字串解析為字典或陣列。

import 'dart:convert';
// 解析程式碼
dataList = json.decode(jsonString);
複製程式碼

但是,我們在專案中使用時,一般都不會直接使用字典取值,這是一種很不好的做法。一般都會將字典或陣列轉換為模型物件,在專案中使用模型物件。可以定義類似Model.dart這樣的模型類,並在模型類中進行資料解析,對外直接暴露公共變數來讓外界獲取值。

自動序列化

但如果定義模型類的話,一個是要在程式碼內部寫取值和賦值程式碼,這些都需要手動完成。另外如果當服務端欄位發生改變後,客戶端也需要跟著進行改變,所以這種方式並不是很靈活。

可以採用json序列化的三方庫json_serializable,此庫可以將一個類標示為自動JSON序列化的類,並對類提供JSON和物件相互轉換的能力。也可以通過命令列開啟一個watch,當類中的變數定義發生改變時,相關程式碼自動發生改變。

首先引入下面的三個庫,其中包括依賴庫一個,以及除錯庫兩個。

dependencies:
  json_annotation: ^2.0.0

dev_dependencies:
  build_runner: ^1.0.0
  json_serializable: ^2.0.0
複製程式碼

定義一個模型檔案,例如這裡叫做User.dart檔案,並在內部定義一個User的模型類。隨後引入json_annotation的依賴,通過@JsonSerializable()標示此類需要被json_serializable進行合成。

定義的User類包含兩部分,例項變數和兩個轉換函式。在下面定義json轉換函式時,需要注意函式命名一定要按照下面格式命名,否則不能正常生成user.g.dart檔案。

import 'package:json_annotation/json_annotation.dart';

// 定義合成後的新檔案為user.g.dart
part 'user.g.dart';

@JsonSerializable()

class User {
  String name;
  int age;
  String email;
  
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}
複製程式碼

下面就是user.dart指定生成的user.g.dart檔案,其中包含JSON和物件相互轉換的程式碼。

part of 'user.dart';

User _$UserFromJson(Map<String, dynamic> json) {
  return User(
      json['name'] as String, json['age'] as int, json['email'] as String);
}

Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
      'name': instance.name,
      'age': instance.age,
      'email': instance.email
    };
複製程式碼

有的時候服務端返回的引數名和本地的關鍵字衝突,或者命名不規範,導致本地定義和伺服器欄位不同的情況。這種情況可以通過@JsonKey關鍵字,來修飾json欄位匹配新的本地變數。除此之外,也可以做其他修飾,例如變數不能為空等。

@JsonKey(name: 'id')
final int user_id;
複製程式碼

現在專案中依然是報錯的,隨後我們在flutter工程的根目錄資料夾下,執行下面命令。

flutter packages pub run build_runner watch
複製程式碼

此命令的好處在於,其會在後臺監聽模型類的定義,當模型類定義發生改變後,會自動修改本地原始碼以適配新的定義。以文中User類為例,當User.dart檔案發生改變後,使用Cmd+s儲存檔案,隨後VSCode會將自定改變user.g.dart檔案的定義,以適配新的變數定義。

系統檔案

主要檔案

  • iOS檔案:iOS工程檔案
  • Android:Android工程檔案
  • lib:Flutter的dart程式碼
  • assets:資原始檔夾,例如font、image等都可以放在裡面
  • .gitignore:git忽略檔案

packages

這是一個系統檔案,Flutter通過.packages檔案來管理一些系統依賴庫,例如materialcupertinowidgetsanimationgesture等系統庫就在裡面,這些主要的系統庫由.packages下的flutter統一管理,原始碼都在flutter/lib/scr目錄下。除此之外,還有一些其他的系統庫或系統資源都在.packages中。

yaml檔案

Flutter中通過pubspec.yaml檔案來管理外部引用,包含本地資原始檔、字型檔案、依賴庫等依賴,以及應用的一些配置資訊。這些配置在專案中時,需要注意程式碼對其的問題,否則會導致載入失敗。

當修改yaml檔案的依賴資訊後,需要執行flutter get packages命令更新本地檔案。但並不需要這麼麻煩,可以直接Cmd+s儲存檔案,VSCode編譯器會自動更新依賴。

// 專案配置資訊
name: WeChat
description: Tencent WeChat App.
version: 1.0.0+1

// 常規依賴
dependencies:
  flutter:125864
    sdk: flutter
    cupertino_icons: ^0.1.2
    english_words: ^3.1.0

// 開發依賴
dev_dependencies:
  flutter_test:
    sdk: flutter
    
flutter:
  uses-material-design: true
  // 圖片依賴
  assets:
    - assets/images/ic_file_transfer.png
    - assets/images/ic_fengchao.png

  // 字型依賴
  fonts:
    - family: appIconFont
      fonts:
        - asset: assets/fonts/iconfont.ttf
複製程式碼

Flutter開發

啟動函式

和大多數程式語言一樣,dart也包含一個main方法,是Flutter程式執行的主入口,在main方法中寫的程式碼就是在程式啟動時執行的程式碼。main方法中會執行runApp方法,runApp方法類似於iOS的UIApplicationMain方法,runApp函式接收一個Widget用來做應用程式的顯示。

void main() {
    runApp()
    // code
}
複製程式碼

生命週期

在iOS中通過AppDelegate可以獲取應用程式的生命週期回撥,在Flutter中也可以獲取到。可以通過向Binding新增一個Observer,並實現didChangeAppLifecycleState方法,來監聽指定事件的到來。

但是由於Flutter提供的狀態有限,在iOS平臺只能監聽三種狀態,下面是示例程式碼。

class LifeCycleDemoState extends State<MyHomePage> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);

    switch (state) {
      case AppLifecycleState.inactive:
        print('Application Lifecycle inactive');
        break;
      case AppLifecycleState.paused:
        print('Application Lifecycle paused');
        break;
      case AppLifecycleState.resumed:
        print('Application Lifecycle resumed');
        break;
      default:
        print('Application Lifecycle other');
    }
  }
}
複製程式碼

矩陣變換

Flutter中是支援矩陣變化的,例如rotatescale等方式。Flutter的矩陣變換由Widget完成,需要進行矩陣變換的檢視,在外面包一層Transform Widget即可,內部可以設定其變換方式。

child: Container(
    child: Transform(
      child: Container(
        child: Text(
          "Lorem ipsum",
          style: TextStyle(color: Colors.orange[300], fontSize: 12.0),
          textAlign: TextAlign.center,
        ),
        decoration: BoxDecoration(
          color: Colors.red[400],
        ),
        padding: EdgeInsets.all(16.0),
      ),
      alignment: Alignment.center,
      transform: Matrix4.identity()
        ..rotateZ(15 * 3.1415927 / 180),
    ),
  width: 320.0,
  height: 240.0,
  color: Colors.grey[300],
)
複製程式碼

Transform中可以通過transform指定其矩陣變換方式,通過alignment指定變換的錨點。

頁面導航

在iOS中可以通過UINavigationController對頁面進行管理,控制頁面間的push、pop跳轉。Flutter中使用NavigatorRouters來實現類似UINavigationController的功能,Navigator負責管理導航棧,包含push、pop的操作,可以把UIViewController看做一個RoutersRoutersNavigator管理著。

Navigator的跳轉方式分為兩種,一種是直接跳轉到某個Widget頁面,另一種是為MaterialApp構建一個map,通過key來跳轉對應的Widget頁面。map的格式是key : context的形式。

void main() {
  runApp(MaterialApp(
    home: MyAppHome(), // becomes the route named '/'
    routes: <String, WidgetBuilder> {
      '/a': (BuildContext context) => MyPage(title: 'page A'),
      '/b': (BuildContext context) => MyPage(title: 'page B'),
      '/c': (BuildContext context) => MyPage(title: 'page C'),
    },
  ));
}
複製程式碼

跳轉時通過pushNamed指定map中的key,即可跳轉到對應的Widget。如果需要從push出來的頁面獲取引數,可以通過await修飾push操作,這樣即可在新頁面pop的時候將引數返回到當前頁面。

Navigator.of(context).pushNamed('/b');

Map coordinates = await Navigator.of(context).pushNamed('/location');
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
複製程式碼

編碼規範

VSCode有很好的語法檢查,如果有命名不規範等問題,都會以警告的形式表現出來。

  1. 駝峰命名法,方法名、變數名等,都以首字母小寫的駝峰命名法。類名也是駝峰命名法,但類名首字母大寫。
  2. 檔名,檔案命名以下劃線進行區分,不使用駝峰命名法。
  3. Flutter中建立Widget物件,可以用new修飾,也可以不用。
child: new Container(
    child: Text(
      'Hello World',
      style: TextStyle(color: Colors.orange, fontSize: 15.0)
    )
)
複製程式碼
  1. 函式中可以定義可選引數,以及必要引數。

下面是一個函式定義,這裡定義了一個必要引數url,以及一個Map型別的可選引數headers

Future<Response> get(url, {Map<String, String> headers});
複製程式碼
  1. Dart中在函式定義前加下劃線,則表示是私有方法或變數。
  2. Dart通過import引入外部引用,除此之外也可以通過下面的語法單獨引入檔案中的某部分。
import "dart:collection" show HashMap, IterableBase;
複製程式碼

=>呼叫

Dart中經常可以看到=>的呼叫方式,這種呼叫方式類似於一種語法糖,下面是一些常用的呼叫方式。

當進行函式呼叫時,可以將普通函式呼叫轉換為=>的呼叫方式,例如下面第一個示例。在此基礎上,如果呼叫函式只有一個引數,可以將其改為第二個示例的方式,也就是可以省略呼叫的括號,直接寫引數名。

(單一引數) => {函式宣告}
elements.map((element) => {
  return element.length;
});

單一引數 => {函式宣告}
elements.map(element => {
 return element.length;
});
複製程式碼

當只有一個返回值,並且沒有邏輯處理時,可以直接省略return,返回數值。

(引數1, 引數2, …, 引數N) => 表示式
elements.map(element => element.length);
複製程式碼

當呼叫的函式中沒有引數時,可以直接省略引數,寫一對空括號即可。

() => {函式實現}
複製程式碼

小技巧

程式碼重構

VSCode支援對Dart語言進行重構,一般作用範圍都是在函式內小範圍的。

例如在建立Widget物件的地方,將滑鼠焦點放在這裡,當前行的最前面會有提示。點選提示後會有下面兩個選項:

  • Extract Local Variable   將當前Widget及其子Widget建立的程式碼,剝離到一個變數中,並在當前位置使用這個變數。
  • Extract Method   將當前Widget及其子Widget建立的程式碼,封裝到一個函式中,並在當前位置呼叫此函式。

除此之外,將滑鼠焦點放在方法的一行,點選最前面的提示,會出現下面兩個選項:

  • Convert to expression body   將當前函式轉換為一個表示式。
  • Convert to async function body   將當前函式轉換為一個非同步執行緒中執行的程式碼。

附加效果

Dart中新增任何附加效果,例如動畫效果或矩陣轉換,除了直接給Widget子類的屬性賦值外,就是在被當前Widget外面包一層,就可以使當前Widget擁有對應的效果。

// 動畫效果
floatingActionButton: FloatingActionButton(
    tooltip: 'Fade',
    child: Icon(Icons.brush),
    onPressed: () {
      controller.forward();
    },
),

// 矩陣轉換
Transform(
  child: Container(
    child: Text(
      "Lorem ipsum",
      style: TextStyle(color: Colors.orange[300], fontSize: 12.0),
      textAlign: TextAlign.center,
    )
  ),
  alignment: Alignment.center,
  transform: Matrix4.identity()
    ..rotateZ(15 * 3.1415927 / 180),
),
複製程式碼

快捷鍵(VSCode)

  • Cmd + Shift + p:可以進行快速搜尋。需要注意的是,預設是帶有一個>的,這樣搜尋結果主要是dart程式碼。如果想搜尋其他配置檔案,或者安裝外掛等操作,需要把>去掉。
  • Cmd + Shift + o:可以在某個檔案中搜尋某個類,但前提是需要提前進入這個檔案。例如進入framework.dart,搜尋StatefulWidget類。

注意點

  • 使用Flutter要注意程式碼縮排,如果縮排有問題可能會影響最後的結果,尤其是在.yaml中寫配置檔案的時候。
  • 因為Flutter是開源的,所以遇到問題後可以進入原始碼中,找解決方案。
  • 在程式碼中要注意標點符號的使用,例如第二個建立Stack的程式碼,如果上面是以逗號結尾,則後面的建立會失敗,如果上面是以分號結尾則沒問題。
Widget unreadMsgText = Container(
    width: Constants.UnreadMsgNotifyDotSize,
    height: Constants.UnreadMsgNotifyDotSize,
    child: Text(
      conversation.unreadMsgCount.toString(),
      style: TextStyle(
        color: Color(AppColors.UnreadMsgNotifyTextColor),
        fontSize: 12.0
      ),
    ),
  );
  
  avatarContainer = Stack(
    overflow: Overflow.visible,
    children: <Widget>[
      avatar
    ],
  );
複製程式碼