該文章屬於<簡書 — 劉小壯>原創,轉載請註明:
<簡書 — 劉小壯> https://www.jianshu.com/p/1a90adc09e99
介紹
Flutter
是Google
開發的新一代跨平臺方案,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
作為開發語言,並且提供Material
和Cupertino
兩套視覺控制元件,檢視或其他和檢視相關的類,都以Widget
的形式表現。Flutter
有自己的渲染引擎,並不依賴原生平臺的渲染。Flutter
還包含一個用C++
實現的Engine
,渲染也是包含在其中的。
Engine
Flutter
是一套全新的跨平臺方案,Flutter
並不像React Native
那樣,依賴原生應用的渲染,而是自己有自己的渲染引擎,並使用Dart
當做Flutter
的開發語言。Flutter
整體框架分為兩層,底層是通過C++
實現的引擎部分,Skia
是Flutter
的渲染引擎,負責跨平臺的圖形渲染。Dart
作為Flutter
的開發語言,在C++
引擎上層是Dart
的Framework
。
Flutter
不僅僅提供了一套視覺庫,在Flutter
整體框架中包含各個層級階段的庫。例如實現一個遊戲功能,上面一些遊戲控制元件可以用上層視覺庫,底層遊戲可以直接基於Flutter
的底層庫進行開發,而不需要呼叫原生應用的底層庫。Flutter
的底層庫是基於Open GL
實現的,所以Open GL
可以做的Flutter
都可以。
視覺庫
在上層Framework
中包含兩套視覺庫,符合Android
風格的Material
,和符合iOS風格的Cupertino
。也可以在此基礎上,封裝自己風格的系統元件。Cupertino
是一套iOS風格的視覺庫,包含iOS的導航欄、button
、alertView
等。
Flutter
對不同硬體平臺有不同的相容,例如同樣的Material
程式碼執行在iOS和Android不同平臺上,有一些平臺特有的顯示和互動,Flutter
依然對其進行了區分適配。例如滑動ScrollView
時,iOS平臺是有回彈效果的,而Android平臺則是阻尼效果。例如iOS的導航欄標題是居中的,Android導航欄標題是向左的,等等。這些Flutter
都做了區分相容。
除了Flutter
為我們做的一些適配外,有一些控制元件是需要我們自己做適配的,例如AlertView
,在Android和iOS兩個平臺下的表現就是不同的。這些iOS特性的控制元件都定義在Cupertino
中,所以建議在進行App開發時,對一些控制元件進行上層封裝。
例如AlertView
則對其進行一個二次封裝,控制元件內部進行裝置判斷並選擇不同的視覺庫,這樣可以保證各個平臺的效果。
雖然Flutter
對於iOS和Android兩個平臺,開發有cupertino
和material
兩個視覺庫,但實際開發過程中的選擇,應該使用material
當做視覺庫。因為Flutter
對iOS的支援並不是很好,主要對Android平臺支援比較好,material
中的UI控制元件要比cupertino
多好幾倍。
Dart
Dart是Google
在2011年推出的一款應用於Web
開發的程式語言,Dart
剛推出的時候,定位是替代JS
做前端開發,後來逐步擴充套件到移動端和服務端。
Dart
是Flutter
的開發語言,Flutter
必須遵循Dart
的語言特性。在此基礎上,也會有自己的東西,例如Flutter
的上層Framework
,自己的渲染引擎等。可以說,Dart
只是Flutter
的一部分。
Dart
是強型別的,對定義的變數不需要宣告其型別,Flutter
會對其進行型別推導。如果不想使用型別推導,也可以自己宣告指定的型別。
Hot Reload
Flutter
支援亞秒級熱過載,Android Studio
和VSCode
都支援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
方法,這就是熱過載的基本過程。
Flutter
的hot reload
的實現原始碼在下面路徑中,在此路徑中包含run_cold.dart
和run_hot.dart
兩個檔案,前者負責冷啟動,後者負責熱過載。
~/flutter/packages/flutter_tools/lib/src/run_hot.dart
複製程式碼
熱過載的程式碼實現在run_hot.dart
檔案中,有HotRunner
來負責具體程式碼執行。當Flutter
進行熱過載時,會呼叫restart
函式,函式內部會傳入一個fullRestart
的bool
型別變數。熱過載分為全量和非全量,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
包含的型別:
- 用於顯示的檢視,例如
ListView
、Text
、Container
等。 - 用來操作檢視,例如
Transform
等動畫相關。 - 檢視佈局相關,例如
Center
、Expanded
、Column
等。
在Flutter
中,所有的檢視都是由Widget
組成,Label
、AppBar
、ViewController
等。在Flutter
的設計中,組合的優先順序要大於繼承,整體檢視類結構繼承層級很淺但單層很多類。如果想定製或封裝一些控制元件,也應該以組合為主,而不是繼承。
在iOS開發中,我也經常採用這種設計方案,組合大於繼承。因為如果繼承層級過多的話,一個是不便於閱讀程式碼,還有就是不好維護程式碼。例如底層需要改一個通用的樣式,但這個類的繼承層級比較複雜,這樣改動的話影響範圍就比較大,會將一些不需要改的也改掉,這時候就會發現繼承很雞肋。但在iOS中有Category
的概念,這也是一種組合的方式,可以通過將一些公共的東西放在Category
中,使繼承的方便性和組合的靈活性達到一個平衡。
Flutter
中並沒有單獨的佈局檔案,例如iOS的XIB這種,程式碼都在Widget
中定義。和UIView
的區別在於,Widget
只是負責描述檢視,並不參與檢視的渲染。UIView
也是負責描述檢視,而UIView
的layer
則負責渲染操作,這是二者的區別。
瞭解Widget
在應用程式啟動時,main
方法接收一個Widget
當做主頁面,所以任何一個Widget
都可以當做根檢視。一般都是傳一個MaterialApp
,也可以傳一個Container
當做根檢視,這都是被允許的。
在Flutter
應用中,和介面顯示及使用者互動的物件都是由Widget
構成的,例如檢視、動畫、手勢等。Widget
分為StatelessWidget
和StatefulWidget
兩種,分別是無狀態和有狀態的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
方法呼叫後,會重新呼叫對應Widget
的build
方法,重新繪製某個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
theme
是Flutter
提供的介面風格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
中可以通過async
、await
組合使用,進行網路請求。Flutter
中的網路請求大體有三種:
- 系統自帶的
HttpClient
網路請求,缺點是程式碼量相對而言比較多,而且對post請求支援不是很好。 - 三方庫
http.dart
,請求簡單。 - 三方庫
dio
,請求簡單。
http網路庫
http
網路庫定義在http.dart
中,內部程式碼定義很全,包括HttpStatus
、HttpHeaders
、Cookie
等很多基礎資訊,有助於我們瞭解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
檔案來管理一些系統依賴庫,例如material
、cupertino
、widgets
、animation
、gesture
等系統庫就在裡面,這些主要的系統庫由.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
中是支援矩陣變化的,例如rotate
、scale
等方式。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
中使用Navigator
和Routers
來實現類似UINavigationController
的功能,Navigator
負責管理導航棧,包含push、pop的操作,可以把UIViewController
看做一個Routers
,Routers
被Navigator
管理著。
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有很好的語法檢查,如果有命名不規範等問題,都會以警告的形式表現出來。
- 駝峰命名法,方法名、變數名等,都以首字母小寫的駝峰命名法。類名也是駝峰命名法,但類名首字母大寫。
- 檔名,檔案命名以下劃線進行區分,不使用駝峰命名法。
Flutter
中建立Widget
物件,可以用new
修飾,也可以不用。
child: new Container(
child: Text(
'Hello World',
style: TextStyle(color: Colors.orange, fontSize: 15.0)
)
)
複製程式碼
- 函式中可以定義可選引數,以及必要引數。
下面是一個函式定義,這裡定義了一個必要引數url
,以及一個Map
型別的可選引數headers
。
Future<Response> get(url, {Map<String, String> headers});
複製程式碼
Dart
中在函式定義前加下劃線,則表示是私有方法或變數。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
],
);
複製程式碼