作為一個Java從業者,並且之前對Flutter甚至安卓、IOS開發無任何經驗,我花了兩週多的時間基本完成一個Flutter APP的開發;從這些天的開發過程來看,Flutter的語法對於Java程式設計師來說應當是比較容易理解和接受的,可能也是因為我對Vue等前端開發也算比較熟悉的原因罷。
Flutter使用的是Dart語言,其本身是基於Dart封裝好的一套SDK;因此在真正使用或學習Flutter之前,建議還是先對Dart的語法進行一個大概的學習,這樣更加有助於提升開發效率,避免多走彎路。
我會主要基於我做這個專案的過程來做一個總結,先對Dart語法與Java不一樣的地方進行一些分析,然後再針對Flutter的一些知識及容易出問題的地方進行總結,當然由於我本身目前只是做了一這個Flutter的專案,對於Dart及Flutter的理解上可能會有不足,歡迎大家一起討論。後續我也會針對自己新的理解對這一系列的文章做更新。
本文主要是從Java與Dart的不同點出發,來講解Dart語言的一些語法與特性,感興趣的可以繼續閱讀。
1. 關鍵字Flutter關鍵字(相對Java)
其它與Java多數是類似的,或者不常使用,使用到時請查閱相關資料。
2. 資料型別2.1 基礎資料型別對應關係(常用)基礎資料型別對應
說明:
包裝型別:Dart沒有包裝型別,int等型別即相當於Java對應的int型別又相當於其對應的包裝型別Integer,因此,我們使用以下語法也是可以的:
int i = 0; print(i.toString());
我們看到i是int型別,可以直接透過i.的方式呼叫一些資料型別的方法;
同樣,以下語法也是正確的:
print(1.toString());
數字型別與字串不能直接相加(而這在Java中是允許的),如以下方式會報錯:
String a = 1 + 'test';
必須使用以下方式:
String a = 1.toString() + 'test';
Extension擴充套件:我們可以透過extension關鍵字來給數字型別或者Dart中的其它型別擴充套件方法,如在我的專案中,有一些圖片地址需要拼接,因此可以往給String定義一個get方法:
extension StringExtension on String { String get img { var image = this; if (image.isEmpty) { return image; } if (!image.startsWith("http")) { image = "http://file.ttcn.vip" + image; } if (!image.contains("~")) { image = image + "~webp"; } return image; }}
然後在使用的地方,可以引入這個檔案,然後直接透過"test".img這樣的方式呼叫:
import 'package:tt_app/common/extend/string_extend.dart';Image image = Image.network(type['image'].toString().img, width: 26.rpx, height: 26.rpx, fit: BoxFit.contain);
同理,我們也可以給Widget增加擴充套件,這個涉及到Flutter的相關元件,後續再細述。
2.2 集合類Dart中集合類主要使用List、Map;
List
相當於Java中的List,他與Java中的List不一樣的是,直接透過new的方式可以建立List物件,而Java必須要透過ArrayList等來例項化;
List list = new List(); List list = [];
List也如java一樣支援泛型:
List<String> list = new List<>();List<dynamic> list = [];
注意上面的dynamic代表列表中可以放所有型別的元素。
List主要有以下常用方法:
List list = [1, 2]; // 計算長度print(list.length); // 增加元素list.add(1); // 批次增加元素 List list1 = [3, 4]; list.addAll(list1); // 獲取指定位置元素var item = list.elementAt(0); item = list[0]; // 定位元素int idx = list.indexOf("test"); // 刪除元素 list.remove("test"); list.removeAt(0); // 子陣列 list.sublist(0, 2); // 元素轉換型別List<String> list = list.map((e) => e.toString()).toList();// 拼接String str = list.join(','); // 直接相加List list = list1 + list2; // 遍歷list.forEach((e) => print(e));// 上面寫法也可以直接簡化:list.forEach(print);// 其它。。。
Map
Map也與Java中的Map對應,與List一樣直接透過New Map就可以完成例項化,如
Map map = new Map(); Map<String, String> map1 = new Map(); Map<String, String> map2 = {};
使用上與Java非常類似。
Map主要用到他的賦值及取值,透過[]運算子進行:
// 初始化Map map = {"test": "a"}; Map map1 = new Map(); // 元素賦值與取值map['test'] = 'a'; print(map['test']); // 判斷是否存在某個元素 bool b = map.containsKey('test'); bool b1 = map.containsValue('a'); // 批次增加map.addAll({"a": "b"});// 為空判斷 bool b = map.isEmpty; bool b1 = map.isNotEmpty; // 刪除元素 map.remove('test'); // 其它...
3. 常用運算子3.1 為空判斷 ??及?.為空判斷主要使用??及?.兩個運算子。
??用於表示如果前一部分值為空,那麼表示式的結果為後部分的值,否則為這前一部分的值,如:
String i; String b = i??'test'; // 值為test String i1 = 'a'; String b1 = i1??'test'; // 值為a
?.表明如果物件不為空,則執行物件的方法,如
int i; print(i?.toString()); // 列印null int i = 1; print(i?.toString()); // 列印1
很多時候,我們可以將兩者結合起來,處理為空時賦預設值的情況:
int i;print(i?.toString()??'test');
3.2 級聯運算子 ..
有時候我們希望連續呼叫一個物件的多個方法,按常規方式我們可以這樣處理:
class Test { void test() { print('test'); } void test1() { print('test1'); } void test2() { print('test2'); }}main() { Test test = new Test(); test.test(); test.test1(); test.test2(); }
這種方式可以透過..運算子來進行簡化:
main() { Test test = new Test(); test ..test() ..test1() ..test2();}
又能夠少敲點程式碼了。
4. 函式4.1 命名函式java中的函式必須要在類中定義(或者匿名方式定義),而dart的函式更類似於js的函式,可以脫離類來進行定義。如我們可以建立一個test.dart的檔案,裡面直接寫函式的實現:
void test() { print('test'); }
在需要使用的檔案中引入該函式所在的檔案就可直接使用:
import 'package:test.dart';main() { test(); }
4.2 匿名函式
函式也可以採用匿名方式定義,如:
// 接收引數型別為函式void test(a) { a(); }main() { test(() => print('test')); // 也可以不使用=> test(() { print('test'); });}
注意上面的=>與{},=>用於函式體中只有一條語句的情況;如果有多條語句必須使用{},而且不能使用=>。
如果是有返回值,=>會將表示式的結果直接當成返回值,如:
void test(a) { print(a());}main() { test(() => 'test'); // 與下面是等價的 test(() { return 'test'; });}
注意看到test方法接收的引數是a,如果我們傳入a不是函式,在編譯時不會出錯,但執行時會出錯,如:test('test'); 會報以下異常:Unhandled Exception: NoSuchMethodError: Class 'String' has no instance method 'call'.
因此我們定義函式的時候,最好帶上引數的型別:
void test(Function a) { print(a());}main() { test(() => 'test'); // 也可以不使用=> test(() { return 'test'; }); // 這句在編輯器中就會直接提示異常 test('test');}
4.3 帶參函式原型定義 typedef使用Function限制類型後,有些場景還無法滿足,假設我們test方法接收的函式a有一個必須的引數,如:
void test(Function a) { print(a('test')); }
如果我們還是跟上面一樣的呼叫:
test(() { return 'test'; });
編輯器不會報錯,但執行的時候會報錯。
這時候我們可以透過typedef 來定義帶參的函式,如:
typedef String Test(String str); // typedef 後的String 表示的是函式的返回型別,括號中的是函式的入參。void test(Test a) { print(a('test'));}main() { test((str){ return str; });}
這個時候我們呼叫的時候傳的函式不帶參,那麼編輯器就會直接提示錯誤。
注意定義匿名函式時,引數前後的()是不可省略的,即使函式沒有任何入參。
4.4 函式其它說明:關於返回型別,可以寫成具體型別或不寫;如void test(){}或者test(){},注意不寫時有些錯誤在編譯時檢查不出來。因此建議能帶型別的時候還是帶上型別。
可變引數:可以透過[]來限定,如:
void test([String a, String b]) { print(a); print(b);}main() { test(); // 列印兩個null test('1'); // 列印1 null test('1', '2'); // 列印1 2 }
命名可變引數:如果可變引數數量極多,可以透過大括號來定義:
void test({String a, String b}) { print(a); print(b);}
呼叫時可以按key: value的方式傳入,如:
main() { test(); // 列印null test(a: '1'); // 列印1 null test(b: '2'); // 列印null 2 test(a: '1', b: '2'); // 列印1 2 }
需要注意的是,如果函式同時有固定引數及可變引數,固定引數需要在可變引數前傳入,如:
void test(String test, {String a, String b}) { print(test); print(a); print(b);}test(); // 會報錯 test('test'); // 不會報錯,列印test null null test('test', a: 1); // 列印test 1 null test('test', b: 2); // 列印test null 2 test('test', a: 1, b: 2); // 列印test 1 2
如果想給可變引數指定預設值怎麼?這時候可以結合:或者=運算子處理:
void test({String a = '1'}) { // 也可以寫成void test({String a: '1'}) {} print(a);}main() { test(); // 列印1 test(a: "2"); // 列印2 }
{}傳參的方式簡直太好用了,尤其是在後續的Flutter開發過程中,我們也會發現Flutter大量元件採用了這種方式。
注意可變引數的使用過程中,如果引數未定義預設值,那麼需要進行非空判斷,這時候就可以使用上文所說的?.及??運算子來處理了。
5. 物件5.1 定義dart中的物件透過class 關鍵字進行定義。如:
class Test { String test; void execute() { print(test); }}void main() { Test test = new Test(); test.test = '1'; print(test.test); test.execute(); }
定義好物件後我們就可以透過New的方式進行例項化了。實際上,new關鍵字可以省略:
Test test = Test();
5.2 私有成員變數
注意Dart中沒有public /private/protected等關鍵字;上面我們按常規定義的變數,在外部是可直接透過例項.的方式訪問的。如上面的test.test,即可透過test.test獲取其值,也可以使用test.test = '1'; 的方式對其進行賦值。
那如果我們想要達到Java中的private同樣的效果如何處理?只需要在變數前面新增下劃線即可:
class Test { String _test; void execute() { print(_test); }}
此時定義的變數只能在類內部使用,外部無法訪問到。
注意變數的作用域,如果是在檔案中定義的私有變數,那麼可以在檔案中使用;類中定義的只能在類中使用;
如果想要在外部對私有變數進行修改與讀取,就只能提供相關的Setter與Getter方法了:
class Test { String _test; get test { return _test; } set test(String test){ this._test = test; }}main() { Test test = Test(); test.test = 'b'; print(test.test); }
同樣的,對於內部使用的方法不想要暴露的,也可以將方法名設定成以下劃線開頭,這樣外部呼叫便看不到這個方法了。
5.3 建構函式常規定義
同樣的Dart中的類也包含有建構函式,常規的建構函式與Java中一樣:
class Test { String _test; Test(String test) { this._test = test; }}
實際上Dart有更加簡潔的語法達到上面的效果:
class Test { String _test; Test(this._test); }
上面直接透過this.成員變數的方式來表示將傳入的引數值賦給對應的成員變數。
與可變引數結合
建構函式也可以與上文所說的可變引數結合,如:
class Test { String _test; String b; String a; Test(this._test, {this.a, this.b});}
在例項化的時候我們就可以透過可變函式呼叫的方式進行例項化:
main() { Test t1 = Test("test"); Test t2 = Test('test', b: 'b'); Test t3 = Test("test", a: "a", b: "b");}
注意可變引數的方式,成員變數不能是私有的,也就是不能以下劃線開頭。這個時候如果我們想要限制外部修改,就需要使用final關鍵字來限制了,如:
class Test { String _test; String b; final String a; Test(this._test, {this.a, this.b});}
這樣定義後,成員變數a就只能在初始化的時候透過建構函式賦值,而不能在例項化後透過例項.a的方式修改其值。
命名建構函式
可以透過類名.的方式使用命名建構函式,如:
class Test { String _test; String b; final String a; Test(this._test, {this.a, this.b}); Test.a({this.a}) { _test = "test"; }}main() { // 例項化時就可以透過以下方式進行例項化: Test test = Test.a(a: "a"); }
注意如果我們沒有定義建構函式,那麼會包括一個預設無參的建構函式;如果定義了,那麼這個無參建構函式就不存在了(與Java是一樣的)。
構造時成員變數賦值
我們可以在建構函式後透過冒號來為成員變數賦值:
class Test { String _test; String b; final String a; Test({this.a}) : _test = "test", b = "b";}
也可以使用建構函式傳入的引數為成員變數賦值:
class Test { String _test; String b; Test(Map params) : _test = params['test'], b = params['b'];}
重定向建構函式
可以透過重定向建構函式來在一個建構函式中呼叫另外一個建構函式進行例項化:
class Test { String _test; String b; Test(Map params) : _test = params['test'], b = params['b']; Test.a():this({"test": "test", "b": "b"});}
5.4 繼承、過載等基礎的使用與Java中很類似,不再展開。
6. 非同步處理Dart中的非同步處理透過Future進行。Future與TS中的Promise有幾分類似。
6.1 典型應用場景一個典型的場景:
Future<bool> test() { return Future(() { sleep(Duration(seconds: 10)); /// 模擬耗時操作 return true; });}main() { test().then(print) .catchError((err) { print(err); }); print('1');}
當我們要非同步處理一個耗時很長的操作時(如網路請求),就可以透過以上方式,使用Future來包裝住這個耗時過程。這樣test方法就會返回一個Future物件,後續我們可以透過then方法實現耗時操作完成時的處理,透過catchError來處理異常。
6.2 等待非同步完成我們可以透過Future的then方法來執行在耗時操作完成後需要進行的處理;但某些情況下我們可能要等待這個非同步處理完成再繼續進行後續的處理; 等待非同步操作完成,可以使用await關鍵字,如:
Future<bool> test() { return Future(() { sleep(Duration(seconds: 10)); /// 耗時操作 return true; });}void test1() async { bool b = await test(); // 等待耗時呼叫完成 print(b); }main() { test1(); }
注意看到我們在test1該當中透過await 來等待test方法執行完成,經過await關鍵字修飾後,會同步等待test方法執行完成並將表示式的結果賦給指定變數。
這樣在test1方法中就完成了等待test方法執行完成然後處理結果的處理。
大家可能會注意到test1方法後面跟了一個async關鍵字。這個關鍵字是告訴呼叫方,在呼叫這個方法的時候自動非同步處理,不需要等待這個方法執行完成。await關鍵字必須使用在被async修飾的方法中,如我們直接在main方法中呼叫await,編輯器會告訴我們不合法!這也是避免主執行緒被耗時操作耗死。在UI中這種基本算是一種常規處理了,否則介面會出現假死現象!
7. 其它7.1 插值表示式如果我們想將一個變數與其它的一些常量字串進行拼接,常規方式下我們一般會這麼寫:
String test = 'a'; print('A的值:' + test);
使用插值可以透過$來在拼接的串中直接使用變數,如:
String test = "a"; print('test的值:$test'); // 也可以使用表示式int a = 10; print('A的值:${a.toString()}');
7.2 var語法var 與js的var類似,表明是一個不確定型別;注意var在賦值後再改變其資料型別就會報錯,這點與js是不一樣的,如:
var i = 1; i = 'test'; // 報錯
多數情況下,我們知道某個變數的型別的時候,不建議使用var,而是直接使用變數的型別如String或者int;最好將var僅用於那些不確定型別的情況,如一個典型的情況,如果map中儲存的型別不確定,也有可能要找的key對應的Value為空,那麼可以透過以下方式處理:
Map map = ...; var item = map['field']; String str = item?.toString();
7.3 print
列印,相當於Java中的System.out.println(),也相當於JS中的console.log();