1.3 Dart語言簡介
在跨平臺開領(lǐng)域被 JS 一統(tǒng)天下的今天,Dart 語言的出現(xiàn)無疑是一股清流。作為后來者,Dart語言有著不少 Java、Kotlin 和 JS 的影子,所以對于 Android 原生開發(fā)者、前端開發(fā)者而言無疑是非常友好。
官方也提供了包括 iOS 、React Native 等開發(fā)者遷移到 Flutter 上的文檔,所以請不要擔(dān)心,Dart 語言不會是你掌握 Flutter 的門檻,甚至作為開發(fā)者,就算你不懂 Dart 也可以看著代碼摸索。
注意:由于本書并非專門介紹Dart語言的書籍,所以本章主要會介紹一下在Flutter開發(fā)中常用的語法特性,如果想更多了解Dart,讀者可以去Dart官網(wǎng)學(xué)習(xí),現(xiàn)在互聯(lián)網(wǎng)上Dart相關(guān)資料已經(jīng)很多了。另外Dart 2.0已經(jīng)正式發(fā)布,所以本書所有示例均采用Dart 2.0語法。
1.3.1 語言特性
Dart所有的東西都是對象, 即使是數(shù)字numbers、函數(shù)function、null也都是對象,所有的對象都繼承自O(shè)bject類。
Dart動態(tài)類型語言, 盡量給變量定義一個類型,會更安全,沒有顯示定義類型的變量在 debug 模式下會類型會是 dynamic(動態(tài)的)。
Dart 在 running 之前解析你的所有代碼,指定數(shù)據(jù)類型和編譯時的常量,可以提高運(yùn)行速度。
Dart中的類和接口是統(tǒng)一的,類即接口,你可以繼承一個類,也可以實(shí)現(xiàn)一個類(接口),自然也包含了良好的面向?qū)ο蠛筒l(fā)編程的支持。
Dart 提供了頂級函數(shù)(如:main())。
Dart 沒有 public、private、protected 這些關(guān)鍵字,變量名以"_"開頭意味著對它的 lib 是私有的。
沒有初始化的變量都會被賦予默認(rèn)值 null。
final的值只能被設(shè)定一次。const 是一個編譯時的常量,可以通過 const 來創(chuàng)建常量值,var c=const[];,這里 c 還是一個變量,只是被賦值了一個常量值,它還是可以賦其它值。實(shí)例變量可以是 final,但不能是 const。
編程語言并不是孤立存在的,Dart也是這樣,他由語言規(guī)范、虛擬機(jī)、類庫和工具等組成:
SDK:SDK 包含 Dart VM、dart2js、Pub、庫和工具。
Dartium:內(nèi)嵌 Dart VM 的 Chromium ,可以在瀏覽器中直接執(zhí)行 dart 代碼。
Dart2js:將 Dart 代碼編譯為 JavaScript 的工具。
Dart Editor:基于 Eclipse 的全功能 IDE,并包含以上所有工具。支持代碼補(bǔ)全、代碼導(dǎo)航、快速修正、重構(gòu)、調(diào)試等功能。
1.3.2 數(shù)據(jù)類型
- num
num 是數(shù)字類型的父類,有兩個子類 int 和 double。
int 根據(jù)平臺的不同,整數(shù)值不大于64位。在Dart VM上,值可以從-263到263 - 1,編譯成JavaScript的Dart使用JavaScript代碼,允許值從-253到253 - 1。
double 64位(雙精度)浮點(diǎn)數(shù),如IEEE 754標(biāo)準(zhǔn)所規(guī)定
int a = 1;
print(a);
double b = 1.12;
print(b);
// String -> int
int one = int.parse('1');
// 輸出3
print(one + 2);
// String -> double
var onePointOne = double.parse('1.1');
// 輸出3.1
print(onePointOne + 2);
// int -> String
String oneAsString = 1.toString();
// The argument type 'int' can't be assigned to the parameter type 'String'
//print(oneAsString + 2);
// 輸出 1 + 2
print('$oneAsString + 2');
// double -> String 注意括號中要有小數(shù)點(diǎn)位數(shù),否則報(bào)錯
String piAsString = 3.14159.toStringAsFixed(2);
// 截取兩位小數(shù), 輸出3.14
- String
Dart里面的String是一系列 UTF-16 代碼單元。
您可以使用單引號或雙引號來創(chuàng)建一個字符串。
單引號或者雙引號里面嵌套使用引號。
用 或{} 來計(jì)算字符串中變量的值,需要注意的是如果是表達(dá)式需要${表達(dá)式}
- bool
Dart 是強(qiáng) bool 類型檢查,只有bool 類型的值是true 才被認(rèn)為是true。
只有兩個對象具有bool類型:true和false,它們都是編譯時常量。
Dart的類型安全意味著您不能使用 if(nonbooleanValue) 或 assert(nonbooleanValue) 等代碼, 相反Dart使用的是顯式的檢查值。
assert 是語言內(nèi)置的斷言函數(shù),僅在檢查模式下有效
在開發(fā)過程中, 除非條件為真,否則會引發(fā)異常。(斷言失敗則程序立刻終止)。
-
List集合
在Dart中,數(shù)組是List對象,因此大多數(shù)人只是將它們稱為List。
Dart list文字看起來像JavaScript數(shù)組文字
//創(chuàng)建一個int類型的list
List list = [10, 7, 23];
// 輸出[10, 7, 23]
print(list);
// 使用List的構(gòu)造函數(shù),也可以添加int參數(shù),表示List固定長度,不能進(jìn)行添加 刪除操作
var fruits = new List();
- Map集合
- 一般來說,map是將鍵和值相關(guān)聯(lián)的對象。鍵和值都可以是任何類型的對象。
每個鍵只出現(xiàn)一次,但您可以多次使用相同的值。Dart支持map由map文字和map類型提供。
- 初始化Map方式一: 直接聲明,用{}表示,里面寫key和value,每組鍵值對中間用逗號隔開。
1.3.3 變量聲明
- var
var 可以定義變量,如 var tag = "666" ,這和 JS 、 Kotlin 等語言類似,同時 Dart 也算半個動態(tài)類型語言,同時支持閉包。var可以接收任何類型的變量,但最大的不同是Dart中var變量一旦賦值,類型便會確定,則不能再改變其類型,如:
var t;
t = "hi world";
// 下面代碼在dart中會報(bào)錯,因?yàn)樽兞縯的類型已經(jīng)確定為String,
// 類型一旦確定后則不能再更改其類型。
t = 1000;
`Dart` 屬于是**強(qiáng)類型語言** ,但可以用 `var` 來聲明變量,`Dart` 會**自推導(dǎo)出數(shù)據(jù)類型**,所以 `var` 實(shí)際上是編譯期的“語法糖”。
-
dynamic和Object
Object是Dart所有對象的根基類,也就是說所有類型都是Object的子類(包括Function和Null),所以任何類型的數(shù)據(jù)都可以賦值給Object聲明的對象.dynamic與var一樣都是關(guān)鍵詞,聲明的變量可以賦值任意對象。而
dynamic與Object相同之處在于,他們聲明的變量可以在后期改變賦值類型。
dynamic t;
Object x;
t = "hi world";
x = 'Hello Object';
//下面代碼沒有問題
t = 1000;
x = 1000;
dynamic與Object不同的是,dynamic聲明的對象編譯器會提供所有可能的組合,
而Object聲明的對象只能使用Object的屬性與方法, 否則編譯器會報(bào)錯。如:
dynamic a;
Object b;
main() {
a = "";
b = "";
printLengths();
}
printLengths() {
// no warning
print(a.length);
// warning:
// The getter 'length' is not defined for the class 'Object'
print(b.length);
}
變量a不會報(bào)錯, 變量b編譯器會報(bào)錯
**`dynamic` 表示動態(tài)類型**, 被編譯后,實(shí)際是一個 `object` 類型,在編譯期間不進(jìn)行任何的類型檢查,而是在運(yùn)行期進(jìn)行類型檢查。
`dynamic`的這個特點(diǎn)使得我們在使用它時需要格外注意,這很容易引入一個運(yùn)行時錯誤.
- final和const
如果您從未打算更改一個變量,那么使用 final 或 const,不是var,也不是一個類型。 一個 final 變量只能被設(shè)置一次,兩者區(qū)別在于:const 變量是一個編譯時常量,final變量在第一次使用時被初始化。被final或者const修飾的變量,變量類型可以省略,如:
//可以省略String這個類型聲明
final str = "hi world";
//final String str = "hi world";
const str1 = "hi world";
//const String str1 = "hi world";
注意 Dart 下的數(shù)值,在作為字符串使用時,是需要顯式指定的。比如:
int i = 0; print("aaaa" + i);這樣并不支持,需要print("aaaa" + i.toString());這樣使用,這和 Java 與 JS 存在差異,所以在使用動態(tài)類型時,需要注意不要把 number 類型當(dāng)做 String 使用。
注意 Dart 中數(shù)組等于列表,所以
var list = [];和List list = new List()可以簡單看做一樣。
1.3.4 函數(shù)
Dart是一種真正的面向?qū)ο蟮恼Z言,所以即使是函數(shù)也是對象,并且有一個類型Function。這意味著函數(shù)可以賦值給變量或作為參數(shù)傳遞給其他函數(shù),這是函數(shù)式編程的典型特征。
每個應(yīng)用程序都必須有一個頂層main()函數(shù),它可以作為應(yīng)用程序的入口點(diǎn)。該main()函數(shù)返回void并具有List<String>參數(shù)的可選參數(shù)。
- 函數(shù)聲明
bool isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
Dart函數(shù)聲明如果沒有顯式聲明返回值類型時會默認(rèn)當(dāng)做dynamic處理,注意,函數(shù)返回值沒有類型推斷:
typedef bool CALLBACK();
//不指定返回類型,此時默認(rèn)為dynamic,不是bool
isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
void test(CALLBACK cb){
print(cb());
}
//報(bào)錯,isNoble不是bool類型
test(isNoble);
- 對于只包含一個表達(dá)式的函數(shù),可以使用簡寫語法
bool isNoble (int atomicNumber)=> _nobleGases [ atomicNumber ] != null ;
- 函數(shù)作為變量
var say = (str){
print(str);
};
say("hi world");
- 函數(shù)作為參數(shù)傳遞
void execute(var callback) {
callback();
}
execute(() => print("xxx"))
- 可選參數(shù)
可選的命名參數(shù)
定義函數(shù)時,使用{param1, param2, …},放在參數(shù)列表的最后面,用于指定命名參數(shù)。例如:
//設(shè)置[bold]和[hidden]標(biāo)志
void enableFlags({bool bold, bool hidden}) {
// ...
}
調(diào)用函數(shù)時,可以使用指定命名參數(shù)。例如:paramName: value
enableFlags(bold: true, hidden: false);
可選命名參數(shù)在Flutter中使用非常多。
可選的位置參數(shù)
包裝一組函數(shù)參數(shù),用[]標(biāo)記為可選的位置參數(shù),并放在參數(shù)列表的最后面:
String say(String from, String msg, [String device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}
下面是一個不帶可選參數(shù)調(diào)用這個函數(shù)的例子:
say('Bob', 'Howdy'); //結(jié)果是: Bob says Howdy
下面是用第三個參數(shù)調(diào)用這個函數(shù)的例子:
say('Bob', 'Howdy', 'smoke signal'); //結(jié)果是:Bob says Howdy with a smoke signal
注意,不能同時使用可選的位置參數(shù)和可選的命名參數(shù)
- 默認(rèn)參數(shù)
函數(shù)可以使用=為命名參數(shù)和位置參數(shù)定義默認(rèn)值。默認(rèn)值必須是編譯時常量。如果沒有提供默認(rèn)值,則默認(rèn)值為null。
下面是為命名參數(shù)設(shè)置默認(rèn)值的示例:
// 設(shè)置 bold 和 hidden 標(biāo)記的默認(rèn)值都為false
void enableFlags2({bool bold = false, bool hidden = false}) {
// ...
}
// 調(diào)用的時候:bold will be true; hidden will be false.
enableFlags2(bold: true);
* 下一個示例顯示如何為位置參數(shù)設(shè)置默認(rèn)值:
String say(String from, String msg,
[String device = 'carrier pigeon', String mood]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
if (mood != null) {
result = '$result (in a $mood mood)';
}
return result;
}
//調(diào)用方式:
say('Bob', 'Howdy'); //結(jié)果為:Bob says Howdy with a carrier pigeon;
- 匿名函數(shù)
大多數(shù)函數(shù)都能被命名為匿名函數(shù),如 main() 或 printElement()。您還可以創(chuàng)建一個名為匿名函數(shù)的無名函數(shù),有時也可以創(chuàng)建lambda或閉包。您可以為變量分配一個匿名函數(shù),例如,您可以從集合中添加或刪除它。
一個匿名函數(shù)看起來類似于一個命名函數(shù) - 0或更多的參數(shù),在括號之間用逗號和可選類型標(biāo)注分隔。
下面的代碼塊包含函數(shù)的主體:
([[Type] param1[, …]]) {
codeBlock;
};
/// 下面的示例定義了一個具有無類型參數(shù)的匿名函數(shù)item,該函數(shù)被list中的每個item調(diào)用,輸出一個字符串,該字符串包含指定索引處的值。
var list = ['apples', 'bananas', 'oranges'];
list.forEach((item) {
print('${list.indexOf(item)}: $item');
});
1.3.5 異步支持
Dart類庫有非常多的返回Future或者Stream對象的函數(shù)。 這些函數(shù)被稱為異步函數(shù):它們只會在設(shè)置好一些耗時操作之后返回,比如像 IO操作。而不是等到這個操作完成。
async和await關(guān)鍵詞支持了異步編程,允許您寫出和同步代碼很像的異步代碼。
Future
Future與JavaScript中的Promise非常相似,表示一個異步操作的最終完成(或失?。┘捌浣Y(jié)果值的表示。簡單來說,它就是用于處理異步操作的,異步處理成功了就執(zhí)行成功的操作,異步處理失敗了就捕獲錯誤或者停止后續(xù)操作。一個Future只會對應(yīng)一個結(jié)果,要么成功,要么失敗。
由于本身功能較多,這里我們只介紹其常用的API及特性。還有,請記住,Future 的所有API的返回值仍然是一個Future對象,所以可以很方便的進(jìn)行鏈?zhǔn)秸{(diào)用。
Future.then
為了方便示例,在本例中我們使用Future.delayed 創(chuàng)建了一個延時任務(wù)(實(shí)際場景會是一個真正的耗時任務(wù),比如一次網(wǎng)絡(luò)請求),即2秒后返回結(jié)果字符串"hi world!",然后我們在then中接收異步結(jié)果并打印結(jié)果,代碼如下:
Future.delayed(new Duration(seconds: 2),(){
return "hi world!";
}).then((data){
print(data);
});
Future.catchError
如果異步任務(wù)發(fā)生錯誤,我們可以在catchError中捕獲錯誤,我們將上面示例改為:
Future.delayed(new Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//執(zhí)行成功會走到這里
print("success");
}).catchError((e){
//執(zhí)行失敗會走到這里
print(e);
});
在本示例中,我們在異步任務(wù)中拋出了一個異常,then的回調(diào)函數(shù)將不會被執(zhí)行,取而代之的是 catchError回調(diào)函數(shù)將被調(diào)用;但是,并不是只有 catchError回調(diào)才能捕獲錯誤,then方法還有一個可選參數(shù)onError,我們也可以它來捕獲異常:
Future.delayed(new Duration(seconds: 2), () {
//return "hi world!";
throw AssertionError("Error");
}).then((data) {
print("success");
}, onError: (e) {
print(e);
});
Future.whenComplete
有些時候,我們會遇到無論異步任務(wù)執(zhí)行成功或失敗都需要做一些事的場景,比如在網(wǎng)絡(luò)請求前彈出加載對話框,在請求結(jié)束后關(guān)閉對話框。這種場景,有兩種方法,第一種是分別在then或catch中關(guān)閉一下對話框,第二種就是使用Future的whenComplete回調(diào),我們將上面示例改一下:
Future.delayed(new Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//執(zhí)行成功會走到這里
print(data);
}).catchError((e){
//執(zhí)行失敗會走到這里
print(e);
}).whenComplete((){
//無論成功或失敗都會走到這里
});
Future.wait
有些時候,我們需要等待多個異步任務(wù)都執(zhí)行結(jié)束后才進(jìn)行一些操作,比如我們有一個界面,需要先分別從兩個網(wǎng)絡(luò)接口獲取數(shù)據(jù),獲取成功后,我們需要將兩個接口數(shù)據(jù)進(jìn)行特定的處理后再顯示到UI界面上,應(yīng)該怎么做?答案是Future.wait,它接受一個Future數(shù)組參數(shù),只有數(shù)組中所有Future都執(zhí)行成功后,才會觸發(fā)then的成功回調(diào),只要有一個Future執(zhí)行失敗,就會觸發(fā)錯誤回調(diào)。下面,我們通過模擬Future.delayed 來模擬兩個數(shù)據(jù)獲取的異步任務(wù),等兩個異步任務(wù)都執(zhí)行成功時,將兩個異步任務(wù)的結(jié)果拼接打印出來,代碼如下:
Future.wait([
// 2秒后返回結(jié)果
Future.delayed(new Duration(seconds: 2), () {
return "hello";
}),
// 4秒后返回結(jié)果
Future.delayed(new Duration(seconds: 4), () {
return " world";
})
]).then((results){
print(results[0]+results[1]);
}).catchError((e){
print(e);
});
執(zhí)行上面代碼,4秒后你會在控制臺中看到“hello world”。
Async/await
Dart中的async/await 和JavaScript中的async/await功能和用法是一模一樣的,如果你已經(jīng)了解JavaScript中的async/await的用法,可以直接跳過本節(jié)。
回調(diào)地獄(Callback Hell)
如果代碼中有大量異步邏輯,并且出現(xiàn)大量異步任務(wù)依賴其它異步任務(wù)的結(jié)果時,必然會出現(xiàn)Future.then回調(diào)中套回調(diào)情況。舉個例子,比如現(xiàn)在有個需求場景是用戶先登錄,登錄成功后會獲得用戶ID,然后通過用戶ID,再去請求用戶個人信息,獲取到用戶個人信息后,為了使用方便,我們需要將其緩存在本地文件系統(tǒng),代碼如下:
//先分別定義各個異步任務(wù)
Future<String> login(String userName, String pwd){
...
//用戶登錄
};
Future<String> getUserInfo(String id){
...
//獲取用戶信息
};
Future saveUserInfo(String userInfo){
...
// 保存用戶信息
};
接下來,執(zhí)行整個任務(wù)流:
login("alice","******").then((id){
//登錄成功后通過,id獲取用戶信息
getUserInfo(id).then((userInfo){
//獲取用戶信息后保存
saveUserInfo(userInfo).then((){
//保存用戶信息,接下來執(zhí)行其它操作
...
});
});
})
可以感受一下,如果業(yè)務(wù)邏輯中有大量異步依賴的情況,將會出現(xiàn)上面這種在回調(diào)里面套回調(diào)的情況,過多的嵌套會導(dǎo)致的代碼可讀性下降以及出錯率提高,并且非常難維護(hù),這個問題被形象的稱為回調(diào)地獄(Callback Hell)?;卣{(diào)地獄問題在之前JavaScript中非常突出,也是JavaScript被吐槽最多的點(diǎn),但隨著ECMAScript6和ECMAScript7標(biāo)準(zhǔn)發(fā)布后,這個問題得到了非常好的解決,而解決回調(diào)地獄的兩大神器正是ECMAScript6引入了Promise,以及ECMAScript7中引入的async/await。 而在Dart中幾乎是完全平移了JavaScript中的這兩者:Future相當(dāng)于Promise,而async/await連名字都沒改。接下來我們看看通過Future和async/await如何消除上面示例中的嵌套問題。
使用Future消除Callback Hell
login("alice","******").then((id){
return getUserInfo(id);
}).then((userInfo){
return saveUserInfo(userInfo);
}).then((e){
//執(zhí)行接下來的操作
}).catchError((e){
//錯誤處理
print(e);
});
正如上文所述, “Future 的所有API的返回值仍然是一個Future對象,所以可以很方便的進(jìn)行鏈?zhǔn)秸{(diào)用” ,如果在then中返回的是一個Future的話,該future會執(zhí)行,執(zhí)行結(jié)束后會觸發(fā)后面的then回調(diào),這樣依次向下,就避免了層層嵌套。
使用async/await消除callback hell
通過Future回調(diào)中再返回Future的方式雖然能避免層層嵌套,但是還是有一層回調(diào),有沒有一種方式能夠讓我們可以像寫同步代碼那樣來執(zhí)行異步任務(wù)而不使用回調(diào)的方式?答案是肯定的,這就要使用async/await了,下面我們先直接看代碼,然后再解釋,代碼如下:
task() async {
try{
String id = await login("alice","******");
String userInfo = await getUserInfo(id);
await saveUserInfo(userInfo);
//執(zhí)行接下來的操作
} catch(e){
//錯誤處理
print(e);
}
}
async用來表示函數(shù)是異步的,定義的函數(shù)會返回一個Future對象,可以使用then方法添加回調(diào)函數(shù)。await后面是一個Future,表示等待該異步任務(wù)完成,異步完成后才會往下走;await必須出現(xiàn)在async函數(shù)內(nèi)部。
可以看到,我們通過async/await將一個異步流用同步的代碼表示出來了。
其實(shí),無論是在JavaScript還是Dart中,
async/await都只是一個語法糖,編譯器或解釋器最終都會將其轉(zhuǎn)化為一個Promise(Future)的調(diào)用鏈。
Stream
Stream 也是用于接收異步事件數(shù)據(jù),和Future 不同的是,它可以接收多個異步操作的結(jié)果(成功或失敗)。 也就是說,在執(zhí)行異步任務(wù)時,可以通過多次觸發(fā)成功或失敗事件來傳遞結(jié)果數(shù)據(jù)或錯誤異常。 Stream 常用于會多次讀取數(shù)據(jù)的異步任務(wù)場景,如網(wǎng)絡(luò)內(nèi)容下載、文件讀寫等。舉個例子:
Stream.fromFutures([
// 1秒后返回結(jié)果
Future.delayed(new Duration(seconds: 1), () {
return "hello 1";
}),
// 拋出一個異常
Future.delayed(new Duration(seconds: 2),(){
throw AssertionError("Error");
}),
// 3秒后返回結(jié)果
Future.delayed(new Duration(seconds: 3), () {
return "hello 3";
})
]).listen((data){
print(data);
}, onError: (e){
print(e.message);
},onDone: (){
});
上面的代碼依次會輸出:
I/flutter (17666): hello 1
I/flutter (17666): Error
I/flutter (17666): hello 3
代碼很簡單,就不贅述了。