Flutter技術與實戰(3)

2020-08-09 10:12:56

Dart語言基礎

基礎語法與型別變數

Dart初體驗
printInteger(int a) {
  print('Hello world, this is $a.'); 
}

main() {
  var number = 2019; 
  printInteger(number); 
}
——————————————————————————————————————————————————————————————————————————————
Hello world, this is 2019. 
Dart的變數與型別
  • 在 Dart 中,我們可以用 var 或者具體的型別來宣告一個變數。當使用 var 定義變數時,表示型別是交由編譯器推斷決定的,當然你也可以用靜態型別去定義變數,更清楚地跟編譯器表達你的意圖,這樣編輯器和編譯器就能使用這些靜態型別,向你提供程式碼補全或編譯警告的提示了。
  • 在預設情況下,未初始化的變數的值都是 null,因此我們不用擔心無法判定一個傳遞過來的、未定義變數到底是 undefined,還是燙燙燙而寫一堆冗長的判斷語句了。
  • Dart 是型別安全的語言,並且所有型別都是物件型別,都繼承自頂層型別 Object,因此一切變數的值都是類的範例(即物件),甚至數位、布爾值、函數和 null 也都是繼承自 Object 的物件。
  • Dart 內建了一些基本型別,如 num、bool、String、List 和 Map,在不引入其他庫的情況下可以使用它們去宣告變數。
num、bool與String
  • Dart 的數值型別 num,只有兩種子類:即 64 位 int 和符合 IEEE 754 標準的 64 位 double。前者代表整數型別,而後者則是浮點數的抽象。
  • 有其他高階運算方法的需求 num 無法滿足,你可以試用一下 dart:math 庫。這個庫提供了諸如三角函數、指數、對數、平方根等高階函數。
int x = 1;
int hex = 0xEEADBEEF;
double y = 1.1;
double exponents = 1.13e5;
int roundY = y.round();
  • 爲了表示布爾值,Dart 使用了一種名爲 bool 的型別。在 Dart 裡,只有兩個物件具有 bool 型別:true 和 false,它們都是編譯時常數。
  • Dart 是型別安全的,因此我們不能使用 if(nonbooleanValue) 或 assert(nonbooleanValue) 之類的在 JavaScript 可以正常工作的程式碼,而應該顯式地檢查值。
// 檢查是否爲0,在 Dart 中需要顯示地與 0 做比較
var number = 0;
assert(number == 0);
// assert(number); 錯誤
  • Dart 的 String 由 UTF-16 的字串組成。和 JavaScript 一樣,構造字串字面量時既能使用單引號也能使用雙引號,還能在字串中嵌入變數或表達式:你可以使用 ${express} 把一個表達式的值放進字串。而如果是一個識別符號,你可以省略{}。
var s = 'cat';
var s1 = 'this is a uppercased string: ${s.toUpperCase()}';
  • 常見字串的拼接,Dart 則通過內建運算子「+」實現。
var s2 = 'Hello' + ' ' + 'World!' ;
  • 對於多行字串的構建,你可以通過三個單引號或三個雙引號的方式宣告,這與 Python 是一致的。
var s3 = """This is a
multi-line string.""";
List與Map
  • 其他程式語言中常見的陣列和字典型別,在 Dart 中的對應實現是 List 和 Map,統稱爲集合型別。它們的宣告和使用很簡單,和 JavaScript 中的用法類似。
var arr1 = ["Tom", "Andy", "Jack"];
var arr2 = List.of([1,2,3]);
arr2.add(499);
arr2.forEach((v) => print('${v}'));
  
var map1 = {"name": "Tom", 'sex': 'male'}; 
var map2 = new Map();
map2['name'] = 'Tom';
map2['sex'] = 'male';
map2.forEach((k,v) => print('${k}: ${v}')); 
  • 容器裡的元素也需要有型別,比如上述程式碼中 arr2 的型別是 List,map2 的型別則爲 Map。Dart 會自動根據上下文進行型別推斷,所以你後續往容器內新增的元素也必須遵照這一型別。
  • 如果編譯器自動推斷的型別不符合預期,我們當然可以在宣告時顯式地把型別標記出來,不僅可以讓程式碼提示更友好一些,更重要的是可以讓靜態分析器幫忙檢查字面量中的錯誤,解除型別不匹配帶來的安全隱患或是 Bug。
  • 如果往 arr2 集閤中新增一個浮點數 arr2.add(1.1),儘管語意上合法,但編譯器會提示型別不匹配,從而導致編譯失敗。
  • 和 Java 語言類似,在初始化集合範例物件時,你可以爲它的型別新增約束,也可以用於後續判斷集合型別。
var arr1 = <String>['Tom', 'Andy', 'Jack'];
var arr2 = new List<int>.of([1,2,3]);
arr2.add(499);
arr2.forEach((v) => print('${v}'));
print(arr2 is List<int>); // true

var map1 = <String, String>{'name': 'Tom','sex': 'male',};
var map2 = new Map<String, String>();
map2['name'] = 'Tom';
map2['sex'] = 'male';
map2.forEach((k,v) => print('${k}: ${v}')); 
print(map2 is Map<String, String>); // true
常數定義
  • 如果你想定義不可變的變數,則需要在定義變數前加上 final 或 const 關鍵字。
    • const,表示變數在編譯期間即能確定的值;
    • final 則不太一樣,用它定義的變數可以在執行時確定值,而一旦確定後就不可再變。
final name = 'Andy';
const count = 3;

var x = 70;  
var y = 30;
final z = x / y;
  • const 適用於定義編譯常數(字面量固定值)的場景,而 final 適用於定義執行時常數的場景。
流程控制語法
  • 對於流程控制語法:如 if-else、for、while、do-while、break/continue、switch-case、assert,由於與其他程式語言類似。
  • 官方文件:https://api.dart.dev/stable/2.2.0/index.html

函數、類與運算子

函數
  • 函數是一段用來獨立地完成某個功能的程式碼。在 Dart 中,所有型別都是物件型別,函數也是物件,它的型別叫作 Function。這意味着函數也可以被定義爲變數,甚至可以被定義爲參數傳遞給另一個函數。
bool isZero(int number) { //判斷整數是否爲0
  return number == 0; 
}

void printInfo(int number,Function check) { //用check函數來判斷整數是否爲0
  print("$number is Zero: ${check(number)}");
}

Function f = isZero;
int x = 10;
int y = 0;
printInfo(x,f);  // 輸出 10 is Zero: false
printInfo(y,f);  // 輸出 0 is Zero: true
  • 如果函數體只有一行表達式,就比如上面範例中的 isZero 和 printInfo 函數,我們還可以像 JavaScript 語言那樣用箭頭函數來簡化這個函數。
bool isZero(int number) => number == 0;

void printInfo(int number,Function check) => print("$number is Zero: ${check(number)}");
  • 有時,一個函數中可能需要傳遞多個參數。那麼,如何讓這類函數的參數宣告變得更加優雅、可維護,同時降低呼叫者的使用成本呢?
  • C++ 與 Java 的做法是,提供函數的過載,即提供同名但參數不同的函數。但 Dart 認爲過載會導致混亂,因此從設計之初就不支援過載,而是提供了可選命名參數和可選參數。具體方式是,在宣告函數時:
    • 給參數增加{},以 paramName: value 的方式指定呼叫參數,也就是可選命名參數;
    • 給參數增加[],則意味着這些參數是可以忽略的,也就是可選參數。
  • 在使用這兩種方式定義函數時,我們還可以在參數未傳遞時設定預設值。
//要達到可選命名參數的用法,那就在定義函數的時候給參數加上 {}
void enable1Flags({bool bold, bool hidden}) => print("$bold , $hidden");

//定義可選命名參數時增加預設值
void enable2Flags({bool bold = true, bool hidden = false}) => print("$bold ,$hidden");

//可忽略的參數在函數定義時用[]符號指定
void enable3Flags(bool bold, [bool hidden]) => print("$bold ,$hidden");

//定義可忽略參數時增加預設值
void enable4Flags(bool bold, [bool hidden = false]) => print("$bold ,$hidden");

//可選命名參數函數呼叫
enable1Flags(bold: true, hidden: false); //true, false
enable1Flags(bold: true); //true, null
enable2Flags(bold: false); //false, false

//可忽略參數函數呼叫
enable3Flags(true, false); //true, false
enable3Flags(true,); //true, null
enable4Flags(true); //true, false
enable4Flags(true,true); // true, true
  • 類是特定型別的數據和方法的集合,也是建立物件的模板。與其他語言一樣,Dart 爲類概念提供了內建支援。
類的定義和初始化
  • Dart 是物件導向的語言,每個物件都是一個類的範例,都繼承自頂層型別 Object。在 Dart 中,範例變數與實體方法、類變數與類方法的宣告與 Java 類似。
  • 值得一提的是,Dart 中並沒有 public、protected、private 這些關鍵字,我們只要在宣告變數與方法時,在前面加上「」即可作爲 private 方法使用。如果不加「」,則預設爲 public。不過,「_」的限制範圍並不是類存取級別的,而是庫存取級別。
class Point {
  num x, y;
  static num factor = 0;
  //語法糖,等同於在函數體內:this.x = x;this.y = y;
  Point(this.x,this.y);
  void printInfo() => print('($x, $y)');
  static void printZValue() => print('$factor');
}

var p = new Point(100,200); // new 關鍵字可以省略
p.printInfo();  // 輸出(100, 200);
Point.factor = 10;
Point.printZValue(); // 輸出10
  • 類的範例化需要根據參數提供多種初始化方式。除了可選命名參數和可選參數之外,Dart 還提供了命名建構函式的方式,

  • 與 C++ 類似,Dart 支援初始化列表。在建構函式的函數體真正執行之前,你還有機會給範例變數賦值,甚至重定向至另一個建構函式。

  • Point 類中有兩個建構函式 Point.bottom 與 Point,其中:Point.bottom 將其成員變數的初始化重定向到了 Point 中,而 Point 則在初始化列表中爲 z 賦上了預設值 0。

class Point {
  num x, y, z;
  Point(this.x, this.y) : z = 0; // 初始化變數z
  Point.bottom(num x) : this(x, 0); // 重定向建構函式
  void printInfo() => print('($x,$y,$z)');
}

var p = Point.bottom(100);
p.printInfo(); // 輸出(100,0,0)
複用
  • 在物件導向的程式語言中,將其他類的變數與方法納入本類中進行復用的方式一般有兩種:繼承父類別和介面實現。
    • 繼承父類別意味着,子類由父類別派生,會自動獲取父類別的成員變數和方法實現,子類可以根據需要覆寫建構函式及父類別方法;
    • 介面實現則意味着,子類獲取到的僅僅是介面的成員變數符號和方法符號,需要重新實現成員變數,以及方法的宣告和初始化,否則編譯器會報錯。
class Point {
  num x = 0, y = 0;
  void printInfo() => print('($x,$y)');
}

//Vector繼承自Point
class Vector extends Point{
  num z = 0;
  @override
  void printInfo() => print('($x,$y,$z)'); //覆寫了printInfo實現
}

//Coordinate是對Point的介面實現
class Coordinate implements Point {
  num x = 0, y = 0; //成員變數需要重新宣告
  void printInfo() => print('($x,$y)'); //成員函數需要重新宣告實現
}

var xxx = Vector(); 
xxx
  ..x = 1
  ..y = 2
  ..z = 3; //級聯運算子,等同於xxx.x=1; xxx.y=2;xxx.z=3;
xxx.printInfo(); //輸出(1,2,3)

var yyy = Coordinate();
yyy
  ..x = 1
  ..y = 2; //級聯運算子,等同於yyy.x=1; yyy.y=2;
yyy.printInfo(); //輸出(1,2)
print (yyy is Point); //true
print(yyy is Coordinate); //true
  • 除了繼承和介面實現之外,Dart 還提供了另一種機制 機製來實現類的複用,即「混入」(Mixin)。

  • 混入鼓勵程式碼重用,可以被視爲具有實現方法的介面。這樣一來,不僅可以解決 Dart 缺少對多重繼承的支援問題,還能夠避免由於多重繼承可能導致的歧義(菱形問題)。

    備註:繼承歧義,也叫菱形問題,是支援多繼承的程式語言中一個相當棘手的問題。當 B 類和 C 類繼承自 A 類,而 D 類繼承自 B 類和 C 類時會產生歧義。如果 A 中有一個方法在 B 和 C 中已經覆寫,而 D 沒有覆寫它,那麼 D 繼承的方法的版本是 B 類,還是 C 類的呢?

  • 要使用混入,只需要 with 關鍵字即可。

class Coordinate with Point {
}

var yyy = Coordinate();
print (yyy is Point); //true
print(yyy is Coordinate); //true
  • 通過混入,一個類裡可以以非繼承的方式使用其他類中的變數與方法。
運算子
  • Dart 和絕大部分程式語言的運算子一樣,所以你可以用熟悉的方式去執行程式程式碼運算。不過,Dart 多了幾個額外的運算子,用於簡化處理變數範例缺失(即 null)的情況。
    • ?. 運算子:假設 Point 類有 printInfo() 方法,p 是 Point 的一個可能爲 null 的範例。那麼,p 呼叫成員方法的安全程式碼,可以簡化爲 p?.printInfo() ,表示 p 爲 null 的時候跳過,避免拋出異常。
    • ??= 運算子:如果 a 爲 null,則給 a 賦值 value,否則跳過。這種用預設值兜底的賦值語句在 Dart 中我們可以用 a ??= value 表示。
    • ?? 運算子:如果 a 不爲 null,返回 a 的值,否則返回 b。在 Java 或者 C++ 中,我們需要通過三元表達式 (a != null)? a : b 來實現這種情況。而在 Dart 中,這類程式碼可以簡化爲 a ?? b。
  • 在 Dart 中,一切都是物件,就連運算子也是物件成員函數的一部分。
    • 對於系統的運算子,一般情況下只支援基本數據型別和標準庫中提供的型別。而對於使用者自定義的類,如果想支援基本操作,比如比較大小、相加相減等,則需要使用者自己來定義關於這個運算子的具體實現。
  • Dart 提供了類似 C++ 的運算子覆寫機制 機製,使得我們不僅可以覆寫方法,還可以覆寫或者自定義運算子。
class Vector {
  num x, y;
  Vector(this.x, this.y);
  // 自定義相加運算子,實現向量相加
  Vector operator +(Vector v) =>  Vector(x + v.x, y + v.y);
  // 覆寫相等運算子,判斷向量相等
  bool operator == (dynamic v) => x == v.x && y == v.y;
}

final x = Vector(3, 3);
final y = Vector(2, 2);
final z = Vector(1, 1);
print(x == (y + z)); //  輸出true

  • operator 是 Dart 的關鍵字,與運算子一起使用,表示一個類成員運算子函數。在理解時,我們應該把 operator 和運算子作爲整體,看作是一個成員函數名。

綜合案例

  • 原始碼地址:https://github.com/cyndibaby905/flutter_core_demo
案例介紹
  • 用 Dart 寫一段購物車程式,但先不使用 Dart 獨有的特性。然後,我們再以這段程式爲起點,逐步加入 Dart 語言特性,將其改造爲一個符合 Dart 設計思想的程式。
//定義商品Item類
class Item {
  double price;
  String name;
  Item(name, price) {
    this.name = name;
    this.price = price;
  }
}

//定義購物車類
class ShoppingCart {
  String name;
  DateTime date;
  String code;
  List<Item> bookings;

  price() {
    double sum = 0.0;
    for(var i in bookings) {
      sum += i.price;
    }
    return sum;
  }

  ShoppingCart(name, code) {
    this.name = name;
    this.code = code;
    this.date = DateTime.now();
  }

  getInfo() {
    return '購物車資訊:' +
          '\n-----------------------------' +
          '\n使用者名稱: ' + name+ 
          '\n優惠碼: ' + code + 
          '\n總價: ' + price().toString() +
          '\n日期: ' + date.toString() +
          '\n-----------------------------';
  }
}

void main() {
  ShoppingCart sc = ShoppingCart('張三', '123456');
  sc.bookings = [Item('蘋果',10.0), Item('鴨梨',20.0)];
  print(sc.getInfo());
}
————————————————————————————————————————————————————————————
購物車資訊:
-----------------------------
使用者名稱: 張三
優惠碼: 123456
總價: 30.0
日期: 2019-06-01 17:17:57.004645
-----------------------------
類抽象改造
  • 在其他程式語言中,在建構函式的函數體內,將初始化參數賦值給範例變數的方式非常常見。而在 Dart 裡,我們可以利用語法糖以及初始化列表,來簡化這樣的賦值過程,從而直接省去建構函式的函數體。
class Item {
  double price;
  String name;
  Item(this.name, this.price);
}

class ShoppingCart {
  String name;
  DateTime date;
  String code;
  List<Item> bookings;
  price() {...}
  //刪掉了建構函式函數體
  ShoppingCart(this.name, this.code) : date = DateTime.now();
...
}
  • 考慮到 兩個類name 屬性與 price 屬性(方法)的名稱與型別完全一致,在資訊表達上的作用也幾乎一致,因此我可以在這兩個類的基礎上,再抽象出一個新的基礎類別 Meta,用於存放 price 屬性與 name 屬性。
class Meta {
  double price;
  String name;
  Meta(this.name, this.price);
}
class Item extends Meta{
  Item(name, price) : super(name, price);
}

class ShoppingCart extends Meta{
  DateTime date;
  String code;
  List<Item> bookings;
  
  double get price {...}
  ShoppingCart(name, this.code) : date = DateTime.now(),super(name,0);
  getInfo() {...}
}
方法改造
  • price屬性的get方法
double get price {
  double sum = 0.0;
  for(var i in bookings) {
    sum += i.price;
  }
  return sum;
} 
  • 在 Dart 中,這樣的求和運算我們只需過載 Item 類的「+」運算子,並通過對列表物件進行歸納合並操作即可實現。
class Item extends Meta{
  ...
  //過載了+運算子,合併商品爲套餐商品
  Item operator+(Item item) => Item(name + item.name, price + item.price); 
}

class ShoppingCart extends Meta{
  ...
  //把迭代求和改寫爲歸納合並
  double get price => bookings.reduce((value, element) => value + element).price;
  ...
  getInfo() {...}
}
  • 在 getInfo 方法中,我們將 ShoppingCart 類的基本資訊通過字串拼接的方式,進行格式化組合,這在其他程式語言中非常常見。而在 Dart 中,我們可以通過對字串插入變數或表達式,並使用多行字串宣告的方式,來完全拋棄不優雅的字串拼接,實現字串格式化組合。
getInfo () => '''
購物車資訊:
-----------------------------
  使用者名稱: $name
  優惠碼: $code
  總價: $price
  Date: $date
-----------------------------
''';
物件初始化方式的優化
  • 在main函數中初始化。
ShoppingCart sc = ShoppingCart('張三', '123456') ;
  • 首先,在對 ShoppingCart 的建構函式進行了大量簡寫後,我們希望能夠提供給呼叫者更明確的初始化方法呼叫方式,讓呼叫者以「參數名: 參數鍵值對」的方式指定呼叫參數,讓呼叫者明確傳遞的初始化參數的意義。在 Dart 中,這樣的需求,我們在宣告函數時,可以通過給參數增加{}實現。
  • 其次,對一個購物車物件來說,一定會有一個有使用者名稱,但不一定有優惠碼的使用者。因此,對於購物車物件的初始化,我們還需要提供一個不含優惠碼的初始化方法,並且需要確定多個初始化方法與父類別的初始化方法之間的正確呼叫順序。
  • 由於優惠碼可以爲空,我們還需要對 getInfo 方法進行相容處理。在這裏,我用到了 a??b 運算子,這個運算子能夠大量簡化在其他語言中三元表達式 (a != null)? a : b 的寫法。
class ShoppingCart extends Meta{
  ...
  //預設初始化方法,轉發到withCode裡
  ShoppingCart({name}) : this.withCode(name:name, code:null);
  //withCode初始化方法,使用語法糖和初始化列表進行賦值,並呼叫父類別初始化方法
  ShoppingCart.withCode({name, this.code}) : date = DateTime.now(), super(name,0);

  //??運算子表示爲code不爲null,則用原值,否則使用預設值"沒有"
  getInfo () => '''
購物車資訊:
-----------------------------
  使用者名稱: $name
  優惠碼: ${code??"沒有"}
  總價: $price
  Date: $date
-----------------------------
''';
}

void main() {
  ShoppingCart sc = ShoppingCart.withCode(name:'張三', code:'123456');
  sc.bookings = [Item('蘋果',10.0), Item('鴨梨',20.0)];
  print(sc.getInfo());

  ShoppingCart sc2 = ShoppingCart(name:'李四');
  sc2.bookings = [Item('香蕉',15.0), Item('西瓜',40.0)];
  print(sc2.getInfo());
}
-----------------------------------------------------------------------
購物車資訊:
-----------------------------
  使用者名稱: 張三
  優惠碼: 123456
  總價: 30.0
  Date: 2019-06-01 19:59:30.443817
-----------------------------

購物車資訊:
-----------------------------
  使用者名稱: 李四
  優惠碼: 沒有
  總價: 55.0
  Date: 2019-06-01 19:59:30.451747
-----------------------------
  • 關於購物車資訊的列印,我們是通過在 main 函數中獲取到購物車物件的資訊後,使用全域性的 print 函數列印的,我們希望把列印資訊的行爲封裝到 ShoppingCart 類中。而對於列印資訊的行爲而言,這是一個非常通用的功能,不止 ShoppingCart 類需要,Item 物件也可能需要。
  • 因此,我們需要把列印資訊的能力單獨封裝成一個單獨的類 PrintHelper。但,ShoppingCart 類本身已經繼承自 Meta 類,考慮到 Dart 並不支援多繼承,我們怎樣才能 纔能實現 PrintHelper 類的複用呢?只要在使用時加上 with 關鍵字即可。
abstract class PrintHelper {
  printInfo() => print(getInfo());
  getInfo();
}

class ShoppingCart extends Meta with PrintHelper{
...
}
  • 經過 Mixin 的改造,我們終於把所有購物車的行爲都封裝到 ShoppingCart 內部了。而對於呼叫方而言,還可以使用級聯運算子「…」,在同一個物件上連續呼叫多個函數以及存取成員變數。使用級聯操作符可以避免建立臨時變數,讓程式碼看起來更流暢。
void main() {
  ShoppingCart.withCode(name:'張三', code:'123456')
  ..bookings = [Item('蘋果',10.0), Item('鴨梨',20.0)]
  ..printInfo();

  ShoppingCart(name:'李四')
  ..bookings = [Item('香蕉',15.0), Item('西瓜',40.0)]
  ..printInfo();
}
完整程式碼
class Meta {
  double price;
  String name;
  //成員變數初始化語法糖
  Meta(this.name, this.price);
}

class Item extends Meta{
  Item(name, price) : super(name, price);
  //過載+運算子,將商品物件合併爲套餐商品
  Item operator+(Item item) => Item(name + item.name, price + item.price); 
}

abstract class PrintHelper {
  printInfo() => print(getInfo());
  getInfo();
}

//with表示以非繼承的方式複用了另一個類的成員變數及函數
class ShoppingCart extends Meta with PrintHelper{
  DateTime date;
  String code;
  List<Item> bookings;
  //以歸納合並方式求和
  double get price => bookings.reduce((value, element) => value + element).price;
  //預設初始化函數,轉發至withCode函數
  ShoppingCart({name}) : this.withCode(name:name, code:null);
  //withCode初始化方法,使用語法糖和初始化列表進行賦值,並呼叫父類別初始化方法
  ShoppingCart.withCode({name, this.code}) : date = DateTime.now(), super(name,0);

  //??運算子表示爲code不爲null,則用原值,否則使用預設值"沒有"
  @override
  getInfo() => '''
購物車資訊:
-----------------------------
  使用者名稱: $name
  優惠碼: ${code??"沒有"}
  總價: $price
  Date: $date
-----------------------------
''';
}

void main() {
  ShoppingCart.withCode(name:'張三', code:'123456')
  ..bookings = [Item('蘋果',10.0), Item('鴨梨',20.0)]
  ..printInfo();

  ShoppingCart(name:'李四')
  ..bookings = [Item('香蕉',15.0), Item('西瓜',40.0)]
  ..printInfo();
}