1、Flutter 介紹與環境安裝
為什么選擇 Dart:
- 基于 JIT 快速開發周期:Flutter 在開發階段采用 JIT 模式,避免每次改動都進行編譯,極大的節省了開發時間
- 基于 AOT 發布包:Flutter 在發布時可以通過 AOT 生成高效的 ARM 代碼以保證應用性能
- UI 幀率可達 120 FPS:為了快速流暢的用戶體驗需要能夠在每個動畫幀運行大量的代碼,不能有周期性的停頓,否則會造成掉幀
- 單線程:不需要鎖,不存在數據競爭和變量狀態同步,也沒有線程上下文切換的性能損耗和鎖導致的卡頓
- 垃圾回收:多生代(參考了 JVM)無鎖垃圾回收器,專門為 UI 框架中常見的大量 Widgets 對象創建和銷毀優化
JIT(Just In Time)即時編譯,在程序執行期間實時編譯為本地機器碼;AOT(Ahead Of Time)靜態編譯,程序運行前編譯成本地機器碼。在代碼的執行效率上,JIT 不如 AOT。
2、Dart 基礎語法
Dart 不用編譯了,dart xxx.dart 直接就執行,不像 Java 需要先 javac 編譯出 class 文件再 java xxx.class 執行。
2.1 變量
類型與聲明
變量都是引用類型,未初始化的變量的值是 null。
聲明變量的方式通常有三種:
- Object:與 Java 一樣 Object 是所有類的基類,Object 聲明的變量可以是任意類型。比如數字(包括 int 類型的數字)、方法和 null 都是對象
- var:聲明的變量在賦值的那一刻,決定了它是什么類型
- dynamic:不是在編譯時候確定實際類型的,而是在運行時。dynamic 聲明的變量行為與 Object 一樣,使用一樣,關鍵在于運行時原理不同
示例代碼:
void test1() {// 1.通過類型聲明變量Object i = "Test";// 2.通過 var 聲明變量var j = "Test";// 報錯,聲明時已經確定了變量類型,不可更改// j = 100;// 3.聲明時沒有具體指明是什么類型,那么就是默認的 Object 類型var k;// 可以為 k 賦值為 Object 的子類型k = "Test";k = 100;// 4.dynamic 動態類型可以賦值不同類型dynamic z = "Test";z = 100;
}
需要注意的地方:
- 所有類型,沒有初始化的變量自動獲取一個默認值為
null
- 聲明變量時,可以選擇加上具體類型,如
int a = 1;
,但對于局部變量,按照 Dart 代碼風格,使用 var 而不是具體類型
final 與 const
final 聲明運行時常量,const 聲明編譯器常量(相比于運行時常量可讓代碼運行更高效),二者都用于聲明常量,可以替代任何類型,只能在聲明時初始化,且不能改變:
void test2() {// 1.const 與 final 可以替代任何類型const a = 1;final b = 1;const int c = 1;final int d = 1;// 2.不能通過運行時常量構造編譯時常量final m = 1;// 企圖通過運行時常量構造編譯時常量,導致 const 值無法確定// const n = m + 1;// 使用編譯時能夠確定的值構造 const 常量是可以的const x = 1;const y = x + 1;
}
類的變量可以是 final 但不能是 const。如果 const 變量在類中,需要定義為 static const 靜態常量:
class T {static const i = 2; // 正確const j = 1; // 錯誤
}
2.2 內置類型
Dart 內置以下類型:
- numbers
- strings
- booleans
- lists(也被稱之為 arrays)
- maps
- runes(用于在字符串中表示 Unicode 字符)
- symbols
數值 num
num 是數字類型的父類,有兩個子類 int 和 double。
int 的默認實現是 64 位,如果編譯成 JavaScript,就是 32 位。在編碼時,如果 int 長度超過 4 個字節,那么 Dart 會將其編譯為類似 Java 的 long 類型,否則編譯成 Java 中的 short 或 int。
也就是說,int 的長度是動態確定的,可以通過 int 的 bitLength() 確定存儲該 int 變量所需要的最小的位數。
但實際上,不應該將 Dart 的 int 和 Java 的 int 做類比,因為前者是一個類,后者是一個基本類型的關鍵字。從本質上說,二者不是一個東西,沒有可比性。
字符串 String
Dart 字符串是 UTF-16 編碼的字符序列,使用方法如下:
-
可以使用單引號或者雙引號來創建字符串:
var name = 'lance'; // 如果插入一個簡單的標識符,而后面沒有緊跟更多的字母數字文本,那么 {} 應該被省略 var a = "my name is $name!"; var b = "my name is ${name.toUpperCase()}!";
-
與 Java 一樣可以使用
+
操作符來把拼接字符串,也可以把多個字符串放到一起來實現同樣的功能:var a = "my name is " "lance";
-
使用三個單引號或者雙引號可以創建多行字符串對象:
var s1 = ''' You can create multi-line strings like this one. ''';var s2 = """This is also a multi-line string.""";
-
可以通過單引號嵌套雙引號,或雙引號嵌套單引號進行轉義:
print("'Test'"); // 'Test' print('"Test"'); // "Test"
-
也可以使用 Java 的方式轉義,或者使用 Dart 的 r 前綴創建一個原始字符串實現轉義:
// 兩行輸出結果均為 換行符 \n print("換行符 \\n"); print(r"換行符 \n");
布爾類型 bool
Dart 的布爾類型 bool 有 true 和 false 兩個對象。
列表 List
Dart 的數組是 List 對象,它有兩種聲明方式:
-
當作 List 對象聲明:
// new 可以省略 var list = new List(1);
-
當作數組聲明:
var list = [1, 2, 3];
通過 for 循環遍歷 List 也有兩種方式:
for(var item in list) {print(item);
}for(var j = 0;j < list.length; ++j) {print(list[j]);
}
當數組與 const 相結合時,需要注意:
List<int> list1 = const[1,2,3];// Unsupported operation: Cannot add to an unmodifiable list//list1.add(4);const List<int> list2 = [1,2];// Error: Can't assign to the const variable 'list2'.//list2 = list1;// Unsupported operation: Cannot add to an unmodifiable list//list2.add(4);
const 修是誰,誰就不可變:
- list1 指向不可變的 [1,2,3],那么就不能修改數組,但是可以指向其他數組對象
- list2 本身是一個常量引用,那么它就只能指向 [1,2],不能修改索引,也不能修改索引的內容
映射集合 Map
兩種聲明方式:
var companys = {'a': '阿里巴巴', 't': '騰訊', 'b': '百度'};
var companys2 = new Map();
// 添加元素
companys2['a'] = '阿里巴巴';
companys2['t'] = '騰訊';
companys2['b'] = '百度';// 獲取與修改元素
var c = companys['c']; // 沒有對應的 key 返回null
companys['a'] = 'alibaba';
const 與 Map 結合的情況與 List 樣。
Runes
Runes 主要用于獲取特殊字符的 Unicode 編碼,或者需要將 32 位的 Unicode 編碼轉換為字符串。
Dart 表達 Unicode 代碼點的常用方法是 \uXXXX,其中 XXXX 是 4 位十六進制值。要指定多于或少于 4 個十六進制數字,需要將值放在大括號中:
var clapping = '\u{1f44f}'; // 5 個 16 進制 需要使用 {}
print(clapping); //👏
// 獲得 16 位代碼單元
print(clapping.codeUnits); // [55357, 56399]
// 獲得 Unicode 代碼
print(clapping.runes.toList()); // [128079]// fromCharCode 根據字符碼創建字符串
print(String.fromCharCode(128079));
print(String.fromCharCodes(clapping.runes));
print(String.fromCharCodes([55357, 56399]));
print(String.fromCharCode(0x1f44f));Runes input = new Runes('\u2665 \u{1f605} \u{1f60e} \u{1f47b} \u{1f596} \u{1f44d}');
print(String.fromCharCodes(input));
這里要清楚一個代碼點和代碼單元的概念:
代碼點(Code Point)和代碼單元(Code Unit)
代碼單元與代碼點
簡言之,代碼點就是字符集中每個字符的值,比如上面代碼中👏符號在 Unicode32 中的值為 0x1f44f。
代碼單元指編碼集中具有最短比特組合的單元。對于 UTF-8 來說,代碼單元是 8 比特長;對于 UTF-16 來說,代碼單元是 16 比特長。換一種說法就是 UTF-8 的是以一個字節為最小單位的,UTF-16 是以兩個字節為最小單位的。
我們在 Java 中常說 String.length() 是獲取字符串長度,實際上是不嚴謹的,應該說是 UTF-16 編碼表示下的代碼單元數量,而不是字符個數。例如:
String a = "\uD83D\uDC4F";
printf(a); // 👏
printf(a.length()); // 2
你看打印輸出的長度為代碼單元個數 2,而不是 a 中字符的個數。charAt() 也是類似的情況。
Symbols
操作符標識符,可以看作C中的宏。表示編譯時的一個常量:
var i = #A; // 常量
print(i.runtimeType); // Symbolmain() {print(i);switch(i) {case #A:print("A");break;case #B:print("B");break;}var b = new Symbol("b");print(#b == b); // true
}
2.3 操作符
主要看 Java 沒有的操作符:
-
類型判定操作符:
is
和!is
用于判斷對象是否為某種類型,as
用于將對象轉換為特定類型 -
賦值操作符:
??=
用來指定值為 null 的變量的值,比如:// 如果 b 是 null,則 value 賦值給 b,否則 b 的值保持不變 b ??= value;
-
條件表達式:
condition ? expr1 : expr2
:如果 condition 為 true 則執行 expr1,否則執行 expr2expr1 ?? expr2
:如果 expr1 不為 null 則取 expr1,否則返回 expr2 的值
-
級聯操作符:
..
可以在同一個對象上連續調用多個函數以及訪問成員變量,這樣可以避免創建臨時變量,代碼看起來也更加流暢:// StringBuffer write() 相當于 Java 的 append var sb = new StringBuffer(); sb..write('foo')..write('bar');
-
安全操作符:
?.
左值如果為 null 則返回 null:String sb; // 報空指針異常 print(sb.length); // 打印輸出 null print(sb?.length);
3、方法
3.1 一等方法對象
Dart 是一個真正的面向對象語言,方法也是對象,類型為Function
。 這意味著,方法可以賦值給變量,也可以當做其他方法的參數。可以把方法當做參數調用另外一個方法。
在 Java 中如果需要能夠通知調用者或者其他地方方法執行過程的各種情況,可能需要指定一個接口,比如 View 的 OnClickListener。而在 Dart 中,我們可以直接指定一個回調方法給調用的方法,由調用的方法在合適的時機執行這個回調:
void setListener(Function listener) {listener("Success");
}// 或者
void setListener(void listener(String result)){listener("Success");
}// 兩種方式,第一種調用者根本不確定回調函數的返回值、參數是些什么
// 第二種則需要寫這么一大段,太麻煩// 第三種:類型定義,將返回值為 void,參數為一個 String 的方法定義為一個類型
typedef void Listener(String result);
void setListener(Listener listener){listener("Success");
}
上面演示了方法作為參數的三種形式:
- 第一種使用 Function 表示一個方法,但是這種形式無法確定方法的參數以及返回值類型,因此不好
- 第二種直接將方法的原型寫在方法參數中,寫起來麻煩,看起來也不舒服,因此也 pass
- 第三種將方法定義為一個類型,使用該類型作為方法參數,推薦這種寫法
3.2 可選命名參數
將方法的參數放到 {}
中就變成可選命名參數:
int add({int? i, int? j}) {if (i == null || j == null) {return 0;}return i + j;
}
調用方法時使用 key-value 形式指定參數:
void main() {print(add()); // 0print(add(i: 1, j: 2)); // 3
}
3.3 可選位置參數
將方法的參數放到 []
中就變成可選位置參數,傳值時按照參數位置順序傳遞:
int add([int? i, int? j]) {if (i == null || j == null) {return 0;}return i + j;
}
調用時可以不傳入全部的參數,參數按照參數聲明的順序賦值:
void main() {print(add()); // 0print(add(1)); // 0print(add(1, 2)); // 3
}
可選命名參數與可選位置參數的出現使得方法重載的實現更容易。在 Java 中,方法重載需要寫出多個不同參數的方法,但是在 Dart 中通過將方法聲明為可選命名參數或可選位置參數,寫一個方法,在調用時傳入所需參數即可。
3.4 默認參數值
定義方法時,可選參數可以使用 = 來定義可選參數默認值:
int add([int i = 1, int j = 2]) => i + j;
int add({int i = 1, int j = 2}) => i + j;
3.5 匿名方法
沒有名字的方法,稱之為匿名方法,也可以稱之為 lambda 或者 closure 閉包。匿名方法的聲明方式為:
([Type] param1, …) { codeBlock;
};
比如:
var list = ['apples', 'oranges', 'grapes', 'bananas', 'plums'];
list.forEach((i) {print(i);
});
4、異常
Dart 的異常機制也像 Kotlin 一樣非常靈活,不像 Java 那樣強制你捕獲異常。
所有的 Dart 異常是非檢查異常,方法不一定聲明了他們所拋出的異常, 并且不要求你捕獲任何異常。
Dart 的異常類型有Exception
和Error
兩種根類型還有若干個它們的子類型,在拋出異常時,可以拋出任何非 null 對象,不局限于Exception
和Error
以及它們的子類型:
throw new Exception('這是一個異常');
throw '這是一個異常';
throw 123;
Dart 雖然也支持 try-catch-finally 捕獲異常,但是 catch 無法指定類型,需要結合 on 使用:
try {throw 123;
} on int catch(e) {// 使用 on 指定捕獲 int 類型的異常對象,on TYPE catch(e)
} catch(e,s) { // 兩個參數的類型分別為 _Exception 和 _StackTracerethrow; // 使用 `rethrow` 關鍵字可以把捕獲的異常給重新拋出
} finally {}
catch() 可以接收兩個參數:
- 第一個參數 e 是被拋出的異常對象,類型是 _Exception
- 第二個參數 s 是堆棧信息對象,類型是 _StackTrace,通過 print(s) 可以輸出異常堆棧信息
騷操作,拋出異常時拋出一個方法,catch 的時候可以通過捕獲 Function 類型來執行該方法。
5、類
Dart 是面向對象的語言,所有類都繼承自 Object。
命名風格:
- 使用 lowercase_with_underscores 風格命名庫和文件名
- 使用 upperCamelCase 命名類型名稱
- 使用 lowerCamelCase 命名其他標識符
- 推薦使用 lowerCamelCase 命名常量
每個實例變量會自動生成一個隱含的 getter 方法,非 final 實例變量還會自動生成一個 setter 方法:
class Point {// 公有變量num x = 0;// _開頭的是私有變量num _y = 0;
}
Dart 在作用域上并沒有 Java 那樣 public、private 的關鍵字,作用域只有公有與私有之分,用 _ 開頭表示私有變量或私有類,不以 _ 開頭的就是公有的類或變量。
5.1 構造函數
常規構造函數
class User {// 初始值一定要給,否則編譯不通過String name = "";int age = 0;User(String name, int age) {this.name = name;this.age = age;}
}
由于把構造函數的參數賦值給實例變量的場景太常見,因此 Dart 提供了語法糖來簡化操作:
class User {String name = "";int age = 0;User(this.name, this.age);
}
也可以使用 {}
將構造函數的參數聲明為可選位置參數,只不過此時不能用 this:
class User {// 成員變量要有初始值String name = "";int age = 0;// 使用可選命名參數,由于 name 和 age 都不可為 null,因此// 參數也需要設置默認值,防止沒有為其傳參時將成員變量賦值為 nullUser({String name = "", int age = 0}) {this.name = name;this.age = age;}
}void main() {var user0 = User(name: "User0"); // name = User0, age = 0var user1 = User(age: 30); // name = , age = 30var user2 = User(age: 22, name: "User2"); // name = User2, age = 22
}
命名構造函數
Dart 不允許任何函數的重載,不論是構造函數還是成員函數還是頂級函數。但有時我們確實有重載構造函數的需求,此時可以使用命名構造函數為一個類實現多個構造函數:
class User {String name = "";int age = 0;User(this.name, this.age);// 命名構造函數,在 . 后面隨意取名,調用時也使用改名字進行構造即可User.fromJson(Map json) {name = json['name'];age = json['age'];}
}void main() {var map = {'name': 'User', 'age': 33};var user = User.fromJson(map);
}
好處是可以通過名字判斷出構造函數的大致意圖和內容,更加直觀。比如 User.fromJson() 就能看出是通過 Json 數據構造 User 對象。
構造函數初始化列表
這一點跟 C++ 很像:
class User {String name = "";int age = 0;User(String name, int age): name = name,age = age;User.fromJson(Map json): name = json['name'],age = json['age'];
}
重定向構造函數
class View {View(int context, int attr);// 會調用上面的構造函數View.a(int context) : this(context, 0);
}
常量構造函數
這里的常量指的是編譯器常量,首先需要使用 const 修飾構造函數:
class ImmutablePoint {final int x;final int y;// 常量構造函數要求成員必須是 final 的const ImmutablePoint(this.x, this.y);
}
然后在構造對象時,不使用 new,而是使用 const,并且要求構造不同對象時傳入的參數必須是一樣的:
void main() {var p1 = const ImmutablePoint(1, 1);var p2 = const ImmutablePoint(1, 1);var p3 = const ImmutablePoint(1, 2);var p4 = new ImmutablePoint(1, 1);print('''p1 == p2:${p1 == p2}
p1 == p3:${p1 == p3}
p1 == p4:${p1 == p4}''');
}
輸出結果為:
p1 == p2:true
p1 == p3:false
p1 == p4:false
主要用于同一個對象被多次使用時,比如 UI 上顯示三個相同的 ListItem,使用常量構造函數就可以創建出一個對象,而不是三個對象,節省了內存。
工廠構造函數
使用 factory 關鍵字修飾,必須返回一個本類或子類的實例對象:
class Person {// 返回本類對象factory Person.get() {return new Person();}// 返回子類對象factory Person.getStudent() {return new Student();}// 如果想要被,需要有一個常規構造函數Person();
}class Student extends Person {}
在 Dart 中使用單例模式時就可以用到 factory:
class Person {// 使用 _ 讓靜態對象私有化,并且類型后面加問號表示為可空// 類型,否則就要在聲明 Person 對象時立即為其初始化static Person? _instance;// 定義工廠構造函數返回單例factory Person.getInstance() {// 如果 _instance 為 null 才創建對象_instance ??= Person._newInstance();// 返回 _instance,后接的感嘆號表示非 null return _instance!;}// 創建一個私有的常規構造函數,這樣默認的構造函數 Person() 就沒有了 Person._newInstance();
}
這樣在 Person 類所在的文件之外,就無法訪問到私有的 _instance 和 _newInstance(),只能通過 getInstance() 獲取到 Person 的單例:
var person = Person.getInstance();
5.2 getter & setter
Dart 中每一個實例屬性都會有一個隱式的 getter,非 final 還有 setter。
首先來看一個錯誤示例:
class Point {int x = 0;int get x => x + 10;
}
在定義 x 的 getter 時編譯器會報錯,說 x 已經定義過。因此如果想自定義屬性的 getter 或 setter 需要將屬性聲明為私有的:
class Point {int _x = 0;int _y = 0;int get x => _x + 10;int get y => _y + 20;
}
在一個需要注意,getter 與 setter 是方法,而不是屬性,因此可以在方法名后面加上 {} 在里面寫相關邏輯:
class Point {int _x = 0;int get x {return _x + 10;}// setter 需要有一個參數set x(int value) {_x = value;}
}
5.3 操作符重載
重載 +
運算符:
class Point {int x = 0;int y = 0;Point(this.x, this.y);// 用 operator 接上要重載的操作符Point operator +(Point other) => Point(x + other.x, y + other.y);
}
這樣可以用 +
連接兩個 Point 對象:
void main() {var point = Point(10, 20) + Point(30, 50);print("x = ${point.x}, y = ${point.y}"); // x = 40, y = 70
}
Dart 的操作符重載非常靈活,返回值的類型不受限制,比如上面重載 +
時返回一個 Point 是我們的常規操作,但是你也可以根據自己需要返回其他類型,比如 String、int 等等。
5.4 抽象類與接口
使用 abstract 定義抽象類,抽象類中允許出現無方法體的方法:
abstract class Parent {String name = "";// 抽象方法前面不能加 abstractvoid printName();
}
Dart 沒有 interface 關鍵字,Dart 中的每個類都隱式定義了一個包含所有實例成員的接口:
class A {void a() {}
}class B implements A {void a() {}
}
5.5 其他語法
可調用的類
如果類中定義了 call 方法,可以通過該類實例對象后接 ()
的形式快速調用 call:
void main() {var a = A();a();
}class A {void call() {print("invoke call method.");}
}
call 方法可以帶參數。
混合 mixins
mixins 是一種在多類繼承中重用一個類代碼的方法,基本形式如下:
void main() {var c = C();c.a();c.b();c.c();
}mixin A {void a() {}
}mixin B {void b() {print("B");}
}class C with A, B {void c() {}void b() {print("C");}
}
注意:
-
被混入的類需要用 mixin 聲明,并且不能有構造函數,否則就無法作為被混入的類出現在 with 后面
-
混合結果的 C 類中,可以重寫,也可以重新定義 A、B 中的方法
-
如果 A、B 內定義了同名方法,且 C 也定義了同名方法,那么 C 的實例在調用該方法時實際上調用的是 C 中的方法;如果 C 中沒有定義同名方法,那么 C 調用的就是 B 中的方法(根據 with 后面的排序,優先取順位靠后的)
-
在 C 中可以通過 super 調用 A 或 B 中的方法,比如:
mixin A {void a() {} }class C with A, B {void a() {super.a();} }
-
上述幾點能發現 mixin 與多繼承的表現有很多相似之處