移動端開發新趨勢Flutter
該文章屬於<簡書 — 劉小壯>原創,轉載請註明:
<簡書 — 劉小壯> 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,);
複製程式碼
網路請求
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], 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
], );
複製程式碼
簡書由於排版的問題,閱讀體驗並不好,佈局、圖片顯示、程式碼等很多問題。所以建議到我Github上,下載Flutter程式設計指南 PDF合集。把所有Flutter文章總計三篇,都寫在這個PDF中,而且左側有目錄,方便閱讀。
下載地址:Flutter程式設計指南 PDF 麻煩各位大佬點個贊,謝謝!?
