2022年6月8日水曜日

Dart 語法測試 + 一點點flutter (未完稿)

一些記述的定義:

... :代表任何可以通過編譯的語法。 (dart沒有這個語法。)

 

Dart:

進入點:  void main()  { ... }

資料概念:

  • 跟javascript一樣,所有的資料都是物件。
  • 變數宣告若不給初始值,都是null。是的,int / bool也一樣...
  • 變數宣告不可在函式或是類別宣告之外

基本資料型態:

  • num:數字。小數與整數是繼承此資料型態而來的
  • int:整數
    isNaN存在,但不可能為true。
  • double:小數
    isNaN存在,但不可能為true。還有 isInfinite跟 isNegative 兩個參數,用來判斷被0除的狀況。
    1.0 == 1 跟 0.0 == -0.0,dart的規定是true。 詳細的性質解說請參考官方文件
  • bool:真偽值  true / false
  • String:頭字「S」必須大寫
    可用陣列操作取單字:String str="abc";  str[1];  // "b"
  • null:變數宣告之後未設值的資料型態
  • var :此參數的型態,在設值的時候依設值的型態決定。這也是宣告參數的時候不加上型態宣告的預設型態。但是之後不可再設定不同資料型態的值。
  • dynamic:可以任意更換資料型態。缺點:不能使用該物件的extension method
  • Object:物件。頭字「O」必須大寫。與var跟dynamic的差異:可使用「Object」的函式跟內部參數。若使用非「Object」的內部參數,編譯會出warning。使用非「Object」的函式,編譯會出錯。
    物件的「相等」,之後會介紹。
  • Function:函式也可以作為參數。頭字「F」必須大寫。
參數宣告語法:  int i = 0; 
基本資料型態,宣告時必須指定資料型態。

 
函式傳入資料:
  • covariant:傳入參數型態的修飾字。代表傳入參數可以變動資料型態
函式回傳資料:
  • void:代表該函式無回傳值。只用在function / method的回傳宣告

靜態:

  • static:在類別內宣告,同類別物件會共享此參數。
    函式定義static,會成為class共享函式。因為沒有實體化,函式內不能使用this。

常數:

  • const: 宣告時必須設值
  • final:宣告時可以不設值,之後只能設一次值。常數宣告之後若有取值動作,在取值之前必須有確切的設值動作,不然編譯會失敗。

dart官方建議可以定義為常數,就儘量定義為常數,可以增加程式碼編譯最佳化的效率。

 

資料型態互轉與判斷:
(以下使用全小寫字代表變數。例如「int」代表整數變數,「str」代表字串變數,「obj」代表物件變數)

資料型態stringintdoublebool
string
int.parse()
str.parseInt()
double.parse()


numnum.toString()



intint.toString()

int != 0

doubledouble.
toStringAsFixed()
double.toInt()



boolbool.toString()bool ? 1 : 0



dynamicdynamic.toString()




Objectobj.toString()




varvar.toString()










空字串:str.isEmpty    //注意:是property(物件的參數),不是function。

字串相加需注意,其他資料型態必須先轉成字串才能相加。就算是int / bool也一樣...

字串插值: '${expression}'  //大括號裡面可以使用一行程式碼做資料處理。若只是從單一變數取值,可以省略大括號

資料型態取得: obj.runtimeType   //注意:是property(物件的參數),不是function。

資料型態判定:  obj is ClassName,回傳bool值。

強制資料型態轉換:obj as ClassName,若不可轉換(轉換來源obj非轉換目標的相關class),編譯會出錯



null 與條件判斷

跟javascript不同的是, if (0) 不會判斷為false。if判斷只有bool型態可以簡寫為沒有判斷子。其他型態都必須有判斷子 ( ==,>,< 等等)

null宣告保護:
String? method(int? property) {}; 代表method可能回傳null值,property可以傳入null。

null取值保護:以「?」作為修飾字。  obj?.property
等同於 「obj != null ? obj.property : null」。可得知還是會回傳null,只是不會取用obj.property,進一步避免null exception。
可以連續使用:obj?.property1?.someMethod() 

可null的宣告:以「?」作為修飾字。  int? i;   //沒有加?的話,因為必須強制設定初值,編譯會出錯。

斷言:在參數名稱的後面加上「!」作為修飾字,保證該參數一定不是null,強制通過編譯。
    例:  obj!.method()  保證obj一定不是null,強制通過編譯。
            建議少用。若是obj為null,會拋出RuntimeException。

流程判斷:若是參數一開始宣告為null,之後在參數取用之前有被設值,就不會被編譯器阻擋。

簡易if指令:   if (a != null) a else b; 可簡化為 a ? a : b ,進一步簡化為 a ?? b

a ??= b :若是a值為null才執行把a設定為b值的動作。 假設
int? i;
i ??= 5;

此時i值為5。 再指定:
i ??= 10;
此時i值仍然為5,因為i已經被設過值。跟javascript不同的是,設值為0也算是「有值」。0並不代表使用if判斷會是「false」。

 

switch-case:特色是case的條件可以描述邏輯判斷。判斷的條件也不需跟switch的參數有關聯。

// Where slash, star, comma, semicolon, etc., are constant variables...
switch (charCode) {
  case slash || star || plus || minus: // Logical-or pattern
    token = operator(charCode);
  case comma || semicolon: // Logical-or pattern
    token = punctuation(charCode);
  case >= digit0 && <= digit9: // Relational and logical-and patterns
    token = number();
  default:
    throw FormatException('Invalid');
}
 

還可以使用類似函式的寫法。例:

token = switch (charCode) {
  slash || star || plus || minus => operator(charCode),
  comma || semicolon => punctuation(charCode),
  >= digit0 && <= digit9 => number(),
  _ => throw FormatException('Invalid')
};

 

Collection(集合)的型態與泛型

定義「集合內的某一資料」的名稱為「元素」。
集合有下列幾種:
  • List: 有順序的資料列。 dart有設定顯式寫法:中括號「[]」
    寫法: List<ClassName> ar = [objOfClassName1, objOfClassName2, objOfClassName3];
  • Set: 不保證順序的資料列。 dart有設定顯式寫法:大括號「{}」
    寫法: Set<ClassName> set = {objOfClassName1, objOfClassName2, objOfClassName3}; 
  • Map: key-value pair。
    dart有定義顯式寫法:大括號「{}」,各個資料必須以 「key : value 」的方式撰寫。注意需使用冒號來定義key與value。

    例:Map<KeyClassName, ValueClassName> map = { objOfKeyClassName1 : objOfValueClassName1, objOfKeyClassName2 : objOfValueClassName2};

Collection類型都是泛型化。Map的key也可以使用實體化的object。key的查找條件就會是「物件的相等(是否為同一物件)」。

宣告為final / const的 List / Set / Map 在宣告同時設值,dart會自動判斷資料型態,因此可以不指定參數型態。 也可以說,不宣告參數型態的參數宣告,必須是初始設值,也一定會成為常數。
例:final Set<int> set = {1,2,3}; 
可以寫成 
set = {1,2,3};

List初始化可以使用陳述定義。  [ for(...) {...}  ] 是允許的。

 

泛型:Class<T extends SomeClassName>  //基本資料型態不支援泛型(class / mixin / interface等等皆支援)。可以使用關鍵字「extends」限定泛型的類別。

泛型函式: T function<T>(Class<T> param)   

  • 意指function定義了泛型「T」(必須的前提)。傳入泛型T的class,回傳T資料型態
  • 為什麼在function後面必須定義「<T>」,是因為這樣才能知道「T」這個字是泛型。不然就會跟class命名還有基本資料型態的命名搞混了。 T這個字也不是固定字,可以為符合參數命名規則的任何文字。另外,泛型可以多個宣告,以逗點隔開。
    例:Map的宣告語法---Map<key, value>


可迭代(可遍歷)集合

基礎class:abstract class Iterable<T>   ,意味著不能直接使用。必須繼承此類別,並實作相關的函式。

 

元素存取

List:取得特定元素,可使用中括號「[]」包住,以數字為index的操作符「ar[int_index]」進行存取。
不支援使用以其他物件做為index的存取(像是javascript可以使用字串做為index)。
增加資料,若集合內已有該資料,會有重複資料。移除資料,可以指定物件,也可以使用index的方式。

Set:無法取得特定元素,只能使用迭代的方式取得元素,因此都是以filter的方式處理。
增加資料,若集合內已有該元素,會以取代的方式處理。移除元素,只能以指定元素的方式。
 
Map:取得特定元素,只能以key物件為index的存取方式,不支援使用數字為index。可以取得keys或是values的迭代,或是使用forEach進行所有Map元素的迭代。
 

集合的狀態

  • isEmpty:是否為空集合
  • isNotEmpty:是否非空集合
  • reversed:反序集合
 
 

單一元素取得

  • first //取得第一個元素。注意:在空集合使用此動作會拋出StateError。
  • last //取得最後一個元素。注意:在空集合使用此動作會拋出StateError。使用此操作較耗資源。猜測內部是使用getter的語法設計。Dart要判斷最少存取的property跟getter的function存取有困難。建議使用命名來分辨。
  • firstWhere(condition)  //回傳第一個符合條件的元素。若是沒有的話會丟出錯誤。
  • firstWhere(condition, T Function? orElse : () )  //回傳第一個符合條件的元素。若是沒有符合的元素,會執行orElse的傳入function,此function必須回傳泛型元素,也可以回傳null。
  • T removeAt(int index):List專用。移除某個順位的元素並回傳該元素。若超出範圍會拋出錯誤RangeError
  • T removeLast(): List專用。移除最後一個元素並回傳該元素。
  • bool remove(T elem):List專用。傳入某個物件,若集合的元素之中有該物件,該集合會移除該元素,並回傳true代表有移除。
  • Tsingle:若是集合只有一個元素,回傳該元素。否則拋出StateError
 

迭代操作

注意:回傳index系列的操作,無符合條件的回傳值都是-1
T代表原始資料型態。T2代表回傳值的資料型態。資料型態可以相同,也可以不同。
elem:集合中的元素
combine / test / action:執行動作的函式。以執行目的來區別。
total: combine函式的傳入值。因為combine的用意是迭代所有元素的組合結果,所以每次都會傳入同一個統計用物件。也會是最後的回傳結果。


這裡只列出Iterable支援的動作:

  • bool any(bool test):使用test函式對集合中的元素測試,是否有任一個元素符合條件
  • bool contains(T elem):集合元素是否含有傳入的物件
  • bool every(bool test):使用test函式對集合中的元素測試,是否都符合條件
  • Iterable<T2> expand(Iterable<T2> action(elem)):對集合每個元素進行操作,每次操作都內部產生一個Iterable<T2>,最後會組合成一個新的Iterable<T2>回傳。
  • T2 fold(T2 initial, combine(T2 result, T elem) ):以initial為初始物件,以combine函式迭代所有元素進行處理,最後回傳result
  • forEach( action(T elem) ):對每個元素進行action的操作。無回傳值。
  • Iterable<T> followedBy(Iterable<T> others) :回傳原集合與others集合連接之後的新集合。傳入集合的資料型態必須與原集合相同。
  • int indexOf(T elem, int start):回傳T物件的集合index。
  • int indexWhere(bool test, int start):使用test函式對集合中的元素測試,回傳第一個符合條件的index
  • int lastIndexOf(T elem, int start):回傳T物件的集合倒數index(從後往前判斷),start為正順序的index。
  • int lastIndexWhere(bool test, int start):使用test函式對集合中的元素測試,回傳倒數第一個符合條件的index(從後往前判斷),start為開始倒數的正順序的index。假設符合條件的index為3,start為2,回傳結果會是-1。
  • String join(String seperator):回傳以seperator隔開的列舉元素字串。只適用於文字集合。或是有自行實作toString()的class。
  • Iterable<T2> map(T2 action(elem)):對集合的每個元素執行action的動作,回傳一個新的集合。回傳的資料型態可以跟原來的集合的資料型態不同。
    注意:回傳的是「lazy」iterable。在回傳的iterable未被其他地方「迭代」之前,map的動作不會被執行。
    是的。若是直接操作回傳的
    「lazy」iterable(像是使用collections.elementAt(0)之類的操作,不會觸發map的動作。)
  • T2 reduce(T2 combine(T2 total, T elem)):使用combine函式迭代所有元素,回傳操作後的結果。
  • T singleWhere(bool test(T elem), T orElse()):使用test函式對集合中的元素測試,若只有一個符合,則回傳該元素。其他條件(沒有元素或是超過兩個以上符合)則執行orElse的動作。若沒有指定orElse,拋出StateError
  • Iterable<T> where(bool test(T elem)):使用test函式對集合內的每個元件進行測試,回傳true的元件將會被放入回傳的新集合。若沒有符合條件的元素,回傳的會是空集合。不會拋出錯誤。原集合不會被更動。
  • void retainWhere(bool test(T elem)):使用test函式對原集合內的每個元件進行測試,回傳true的元件將會被保留,否則會被移除。
  • void sort(int test(T elem1, T elem2)) :使用test函式進行氣泡排序法。兩兩比較。回傳負數,elem1會在elem2前面。回傳0,順序不變。回傳正整數,elem2會在elem1前面。
  • Iterable<T> skip(int count):回傳略過指定數量的新集合
  • Iterable<T> skipWhere(bool test(T elem)):使用test函式對集合中的元素測試,回傳第一個符合測試條件之後所有元素的新集合
  • Iterable<T> take(int count):回傳指定數量的新集合
  • Iterable<T> takeWhile(bool test(T elem)):使用test函式對集合中的元素測試,回傳第一個不符合測試條件之前的所有元素的新集合

 

 

 

 


函式 / 方法(function / method)

「方法」這個字通常用在class定義。後文也會以這樣的方式做陳述。
 

基本定義

ReturnClassName function1(ClassName1 param1, ClassName2 param2) { return objOfReturnClassName; }  //注意大括號後面不要使用分號

箭式定義

ReturnClassName function1(ClassName1 param1, ClassName2 param2) => objOfReturnClassName;

參數定義

ReturnClassName
Function(ClassName1 param1, ClassName2 param2) function1 = (ClassName1 param1, ClassName2 param2) { return objOfReturnClassName;} ;   //注意尾端一定要有分號
參數定義也可以寫成箭式:
ReturnClassName
Function(ClassName1 param1, ClassName2 param2) function1 = (ClassName1 param1, ClassName2 param2) => objOfReturnClassName;

當做參數傳入函式

int plus1(int a) { return a+1;}
void doSomething(int Function(int a) f)
{
  f(a);
}

使用:
doSomeThing(plus1(1));  //只要輸入輸出的參數type正確就可以使用

List<int> list = [1,2,3];
list.forEach(plus1);  //只要輸入輸出的參數type正確就可以使用

匿名函式

通常用在只定義一次的地方。
(Class1 : param1, Class2 : param2) {
    return obj;
}
 

箭式定義跟javascript的不同點

  • 在大小括號內定義的話,不可使用指令結束的分號(只能有一行)
  • 在箭頭後面使用大括號{},會是回傳Set或是Map的形式,不會是定義function。
    例:
    ReturnClassName function1(ClassName1 param1, ClassName2 param2) => {objOfReturnClassName};   //編譯錯誤
    Set<ReturnClassName> function1(ClassName1 param1, ClassName2 param2) => {objOfReturnClassName};   //可通過編譯

變數的作用域

dart的設計是只要在該變數定義的同一層大括號內的所有指令,包括函式的下層,一定可以用到。

回傳值

所有的函式一定有回傳值。函式定義的大括號內沒有寫return的話,就會return null。


Cascades(串接,級連)

若是要連續操作同一個物件,可以使用串接的寫法。

return obj.property1 = value1
..method2()
..property2 = value2
..method3();
注意:串接寫法,各指令之間不可以有代表指令結束的分號。建議串接符號「..」前面有空格,防止誤判與誤讀。

基本上script語言非常依賴function,就不要省,儘量使用。

串接動作的回傳值是被串接的「物件」(也就是連續動作的起點物件),以上例來說,回傳值會是「obj」。

串接+箭式+setter的組合應用: 
以下可以表示「對每一個集合元素的property設置為1,並回傳一個操作過的集合」
collection.map((elem) => elem..property = 1 )

 

傳入參數的方式:基本的「循序」 / 不循序的「指定參數名」。

循序傳入的定義與使用

例: 定義:function(Class1 param1, Class2 param2) {;}   
       使用:function(objOfClass1, objOfClass2);  //參數不可以多傳入也不可以少傳入

循序傳入的可變傳入參數

使用中括號定義。必須放在固定傳入參數的後面。可變的傳入參數,可以給初始值。
這樣若是使用時沒有傳入的話,就會使用初始值。
若不給初始值,必須宣告為nullable
例: function(int param1, [String param2 = "", int? param3]) {;}
使用:function(3, 5);   //此時param2會是「""」(空字串)

 ============================

指定參數名的定義與使用

定義函式,指定參數名的定義必須使用大括號包住。
使用函式,傳入值則不需使用大括號,但是必須指定參數名。
例: 定義:function({Class1 : param1, Class2 : param2}) {;}   
       使用:function(param1 : objOfClass1, param2 : objOfClass2); 

指定參數名的可變傳入參數

function({required Class1 : param1, Class2? : param2 = default2 , Class3 : param3}) {;}
指定參數名的傳入參數,預設是非必須傳入。可用「required」關鍵字定義為必須傳入。


初始化表列

在傳入參數的小括號後面加上冒號,可以定義參數初始化的動作。會在function執行之前做處理。可以用在對參數內的「final」定義參數做設定。可以避免final參數在使用前未被設值的編譯錯誤,或是參數的數值防呆。
冒號後面接的指令範圍是一個分號之內可以表達的動作。可以多個動作,使用逗號隔開。運行順序是循序的。
例:  function (int param1, String param2) : assert(param1 > 0), assert( param2.length > 0) {;}


類別(class / mixin / extension)

定義範例

class A {
  int _property1 = 0;
  int property2 = 0;
  A() {}   //預設constructor。也可以帶參數。但是帶參數的話,繼承的子類別必須以初始化表列(冒號)的方式call父類別的帶參數建構子做初始化。

  A.B(int value1 = 0) {   //其他constructor必須指名初始化的名稱,不可同名過載
    _property1 = value1;
  }

  A.C(this._property1);  //上面的A.B的動作的簡寫。包含傳入值的宣告為_property1的參數型態int,與把_property1設值為傳入值。小括號內須註明this的原因是「constructor算是在物件外部」

  A.D() : this.B(999);  //利用函式的「初始化表列」的方式使用其他constructor。初始化表列的階段已經可使用「this」,因為物件的建立的步驟是「分配記憶體,執行constructor」。在執行constructor之前,記憶體已分配好。就可使用「this」。不寫this的話,會變成取用static method。若沒定義,就會編譯錯誤。

  A.E({this.property2 = 0});  //指定參數名的不定傳入參數

  A copyWith({int? : value1}) {  //class method。此例為「不定可null」的傳入值,進行轉換為「null safe」回傳值的寫法。
    return A.E(property2 : value1 ?? this.property2)
  }

  int get property { 
    //getter:不可跟內部參數同名,取值的時候如同一般的參數取值,因為無傳入值,不需小括號。必須指定回傳資料型態。
   return _property1 + 2;
  }

  set property { 
    //setter:不可跟內部參數同名,設值的時候如同一般的參數設值,因為傳入值必為宣告名稱,不需小括號。
    _property1 = property;
  }

  int sum()  //物件方法範例
  {
    return _property1 + property2;
  }

  static A Plus1(A a)  //靜態方法範例(靜態方法名稱建議頭字大寫)
  {
    a._property1++;
    a.property2++;
    return a;
  }

  @override
  int get hashCode => Object.hash(_property1, property2); 
  //再定義物件的「相等」運算子的範例。dart的「物件相等」是使用hashCode的「相等」做比較。hashCode的預設值是在物件被建立的時候自動產生的一個內部數值,所以每個物件都會不一樣。自定義的話,就可能會造成不同的物件擁有相同的hashCode。相信也就是必須這樣設計,才會動用這個部分。要注意的是,這部分的動作越簡單越好。因為集合的處理會大量執行這個操作,過於複雜會大幅影響集合處理的效率。
//另外,若重新定義hashCode這個getter,建議也需要重新定義運算子「==」,以確保「相等」的意義相同。


  @override
  bool operator ==(dynamic other)  //再定義物件的「相等」運算子的範例
  {
    return other is A  && (other._property1 == property1)
      && (other.property1 == property2);
  }
}


建構 / 實體化

使用預設建構子(constructor)實體化:  ClassName();    //(不需使用「new」文字)
在繼承的情況之下,父類別只有預設建構子會被使用,且一定會被執行。似乎是無法在子類別使用父類別的其他建構子。也就是類似「super.ParentClassConstructor(...)」的動作。

建構子可以寫「return」,但不可以回傳任何東西。(因為一定會return 「this」)

其他建構子(非預設建構子)的實體化使用方法:   A.B(1);
傳入參數(括號內)的定義規則,與函式的傳入參數相同。
也可以使用不定參數的設計。(因為dart不允許靜態方法過載。建構子的定義為「靜態方法」)

建構子的傳入參數定義,可以直接對建構物件的內部參數設值(需使用this指定內部參數名):
A.B(int value1 = 0) { _property1 = value1; }
可以簡寫為「A.B(this._property1);」

factory 建構子:在建構子的命名之前,加上關鍵字「factory」。特性是回傳的資料型態可以為子class,或是null。必須與預設/其他建構子的命名不同。後面會有抽象類別與工廠建構子的使用範例。通常用在不是每次都要實體化一個新的物件的情況(像是有個pool管理,循環使用有限資源之類)。

可利用函式的「初始化表列」的方式,進行使用其他建構子的動作。(上面的範例有提到寫法)

常數建構:在建構子之前加上「const」修飾。注意:const class的內部參數,必須全都有final修飾


類別成員

  • 物件成員:可以定義變數與方法。以一般的變數與函式的方式定義。物件變數在實體化之後,每個物件裡面皆有一份獨立的資料。
  • 靜態成員:所有同類別的物件共用一份資料。使用「static」關鍵字做修飾。
    靜態方法的內容定義,因為是類別共用,不可使用this(無法分別是哪個物件)。
    使用靜態方法:「className.staticFunction()」


getter / setter (物件變數的存取方法)

覺得比較像是語法糖的做法...

  int get property {
    return _property1 + 2;
  }
  set property(int value1) {
    _property1 = value1;
  }


注意:

  • get必需定義回傳資料型態,set不需要。get不指定回傳資料型態的話,預設資料型態是dynamic
  • 同名的存取方法,傳入與回傳的資料型態必須相同。傳入參數只能一個。
  • 存取方法不可與class的內部參數同名
  • 存取方法的函式定義內部若是有變數命名,不可使用存取方法的命名。會造成runtime錯誤。
  • get不可定義傳入參數的小括號,set必須定義傳入參數的小括號。
  • getset都可以使用箭式寫法。在此不需寫小括號。上例可簡化成
    int get property => _property1 + 2;
    set property(int value1) => _property1 = value1;


類別繼承

類別繼承指令
多重繼承定義建構子
(實體化)
定義變數函式重載
classextends
implements
不可

必須
不可

需實作getset
依需求
必須重載
abstract classextends
implements
不可
不可

需實作getset
必須重載
mixin
mixin class
with
不可


覆蓋
extension
on
不可
只能有
靜態變數
不可






多重繼承定義的先後順序:  extends -> with -> implements

 
 

抽象類別(abstract class)與工廠建構子(factory constructor)

  • 不可實體化
  • 只可以定義方法的命名與輸出入參數,不可定義方法本身
範例:
factory建構子的一般使用法:「factory 預設建構子.非預設建構子」
 
abstract class A {
  A();  //預設建構子

  factory A.obtain()  // factory建構子與預設建構子不同名(避免循環參照)
  { return B(); }
}

class B extends A {
  B();
}

//這時可以使用 A.obtain() 取得實體化的物件

//----------------------------factory建構子的另一種使用法:factory 建構子做為預設建構子
abstract class C {
  C.c();  //一般建構子

  factory C(){   //factory建構子為預設建構子
    return D();
  }
}

class D extends C {
  D() : super.c();  //必須在初始化表列使用抽象類別的建構子
}
//這時可以使用 C() 取得實體化的物件

  • 不可實體化。但是可以經由factory constructor實體化。
  • 使用factory constructor的限制:抽象類別必須定義不同名的建構子(避免循環參照)。
    若是要同名,必須在子類別建構子的初始化表列使用抽象類別的建構子(避免未建構)

 

 

介面(interface)

  • 介面的定義是class的「所有方法」跟「所有參數的getset」。
  • 介面繼承:class以「implements」關鍵字就可以只繼承介面。這時建議把父類別看成抽象類別,就可以理解該做哪些事情。
  • 介面繼承,必須定義父類別的內部參數的getset(此時就必須跟父class的參數名相同。但是不能使用父class的內部參數...)。 
  • 介面繼承可以多重。

 

mixin 

  • 在要加入mixin的類別,使用「with」關鍵字指定要加入的mixin,可以多重。
  • 概念是「被mixin的類別,會擁有mixin類別所定義的函式」
  • 一般類別也可以加上「mixin」修飾做為mixin。成為「mixin class」。
  • 可限定被繼承的class。使用「on」關鍵字指定可繼承此mixin的class。這時
    限定繼承class的子class也可以繼承。
  • 使用mixin的類別,若有參數命名跟mixin內的同名,會使用類別的參數定義。
  • 使用mixin的類別,若有函式命名跟mixin內的同名,會使用類別的函式定義。
    若沒有同名的函式,但是多重mixin裡面有同名,採取後定義蓋掉前定義的方式。
    若使用「super.function();」的方式呼叫,會執行前一個定義。
  • 不可獨自實體化。但是定義為「mixin class」則可以獨自實體化。
  • 可定義內部參數。被mixin的類別無法直接存取。
  • 不可繼承其他類別與mixin

 定義範例:

class Animal {}

mixin CanRun {
  int leg = 0;
  void run() {
    print("run");
  }
}

mixin class Leg {
  int leg = 1;
  void run() {
    print("Run with ${leg} legs");
  }
}

class Bird extends Animal with CanRun, Leg {
  int leg = 2;
}

class Dog extends Animal with CanRun, Leg {
  int leg = 4;
  void run() {
    print("Dog Run");
  }
}
 
Bird().run();  //Run with 2 legs
Dog().run();  //Dog Run


 

extension

dart 2.7版導入的新定義。
相對於mixin的在class定義階段指定擴張,extension可以在class定義之後,從外部對class做定義擴張。

  • 使用on關鍵字指定擴張對象
  • 不可重載class的函式,編譯期間若是遇到函式命名重疊會產生編譯錯誤,必須避開
  • 可定義靜態變數。只有extension的函式看得到,被擴張的class看不到。
  • extension定義的函式,輸入輸出參數不可使用dynamic定義
  • 可以使用被擴張對象class的所有成員
  • 不可獨自實體化

定義範例:假設想要讓String也能夠parse數字
extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }

  double parseDouble() {
    return double.parse(this);
  }
}

 

 

方法重載(再定義)

  • 子類別可以對繼承的父類別的「物件方法」再定義(不可再定義靜態方法)
  • 建議加上「@override」標記,標明是重載。
  • 在函式內可以使用「super.function();」的方式呼叫,會執行前一個定義。
    若是只有繼承,就是執行父類別的「function()」所定義的動作。
    若是「function()」有被mixin的同名函式重載,會執行最後一個「function()」定義的動作。
  • covariant:函式重載(override)專用修飾字,表示傳入資料可以更換資料型態

 

可執行物件

在class裡面定義 「call(...)」就可以讓物件當做函式執行。
例:
class A { String call (String a, String b) => "$a $b"; }
A a = A();
print(  a( "aaa", "bbb")  );


物件的「相等」

內部實作是以「object.hashCode」做比較。類別可以再定義操作子「==」。但是重定義操作子的話,建議getter「hashCode」也必須再定義。



別名

別名宣告: typedef 可以簡化宣告類型,容易理解。

typedef ListMapper<X> = Map<X, List<X>>;
Map<String, List<String>> m1 = {}; // Verbose.
ListMapper<String> m2 = {}; // Same thing but shorter and clearer.


例外處理

定義

跟其他語言的寫法類似。 使用「on」關鍵字限定例外class。

try {
  breedMoreLlamas();
} on OutOfLlamasException {  //可以不寫「catch」,不過就拿不到錯誤訊息的詳細資訊
  // A specific exception
  buyMoreLlamas(OutOfLlamasException);  //可以把未實體化的class當做已實體化的物件使用... 神奇。
} on Exception catch (e) {
  // Anything else that is an exception
  print('Unknown exception: $e');
} catch (e) {
  // No specified type, handles all
  print('Something really unknown: $e');
} finally {
  // Always clean up, even if an exception is thrown.
  cleanLlamaStalls();
}

丟出例外

寫法:
throw ExceptionClass();  //其實就是實體化一個class並丟出。除了null之外都可以丟。
rethrow;  //再往外丟







異步執行/ 響應式處理Reactive Programming

現在比較新的ui開發環境都走向響應式處理的設計思路。
Dart也不例外。除了main()之外,在使用class宣告建立app之後,就進入了響應式處理的環境。因為web主要的操作:ui回應跟網路取得資料,甚至是vSync這類的動畫重繪需求,都是以響應式處理做為設計方式。

異步執行的基礎: Future (類似javascript的Promise)

目的:將函式封裝成異步執行的動作。

定義方式:Future<datatype1>  futureHandler = Future(function);
後續使用:futureHandler.then((datatype1 value1) { ... }).then((datatype2 value2) { ... });

注意:在宣告Future(function)的同時會call function
若是後續有指定then的動作,才會執行then裡面的動作。 

Dart官方建議儘量減少try / catch寫法,使用future-then-catcherror的方式取代。
範例:

// Synchronous code.
try {
  int value = foo();
  return bar(value);
} catch (e) {
  return 499;
}

Equivalent asynchronous code, based on futures:
Future<int> asyncValue = Future(foo);  // Result of foo() as a future.
asyncValue.then((int value) {
  return bar(value);
}).catchError((e) {
  return 499;
});

要注意的是與同步函式的銜接。因為同步函式裡面使用異步設計,會先return,導致持續運作。

若是有個流程,設計上必須等待前面的執行完成,要把後續處理也以異步的方式設計。
通常會採用封裝成一個functon的方式處理,然後用then連接下去,就可以達到目的。

asyncValue = Future.sync(function)  //會強制同步執行function再往下執行。但是asyncValue.then()的動作會是異步。

 

 
Future的各種方法

方法註釋blocking
(循序執行)
then
條件
Future(FutureOr<T> action())
執行一次actionno執行後(非同步)
sync(FutureOr<T> action())
執行一次actionyes
執行後(非同步)
doWhile(FutureOr<bool> action())
持續重複執行
至回傳false
yes
回傳false之後
Future<dynamic>
any(Iterable<Future<T>>)
遍歷執行一次
no
其中一個Future return
wait(Iterable<Future<T>>)
遍歷執行一次no
所有的Future都return之後
microtask(FutureOr<T> action())執行一次action
保證當時優先執行
no
執行後(非同步)








*FutureOr<T>:回傳值可能是Future也可能是泛型T

 

Generator:分成同步與異步兩種。需使用 「yield」指令代替return。yield的設計並不像一般函式的return是強制一定要回傳值,可以在需要的情況下才往外傳值。

  • 同步Generator:   Iterable<T> generator(...) sync* { if (...) yield obj;}
  • 異步Generator:   Stream<T> generator(...) async* { if (...) yield obj;}


Completer:具有後執行的能力。可與Future配合使用,在需要回傳Future的地方,回傳「Completer.future」。

Stream:具有「維持」與「多值回傳」的能力。function不能實體化,只能使用一次,回傳一個資料。Stream可以物件(實體)化,在其他可以取得物件的地方供使用,可以回傳多個資料。

預設的實作方式是只能有一個接收回傳資料者。有提供BroadcastStream,就可以實作為可以容納多個接收者的設計。

具有「要求暫停發送」的功能,RxJS沒有這樣的設計。

這是RxJS與Dart的比較。供參考。
Cold跟Hot的區別是「cold是資料已經固定的資料源,hot是在觀察的時間點資料未固定,還可能會送出資料的資料源」
狀況RxJS Observable
Dart Stream
An error is raised
Observable Terminates with Error
Error is emitted and Stream continues
Cold Observables
Multiple subscribers can listen to the same cold Observable, each subscription will receive a unique Stream of data
Single subscriber only
Hot Observables
Yes
Yes, known as Broadcast Streams
Is {Publish, Behavior, Replay}Subject hot?
Yes
Yes
Single/Maybe/Complete ?
Yes
No, uses Dart Future
Support back pressure
Yes
Yes
Can emit null?
Yes, except RxJavaYes
Sync by default
YesNo
Can pause/resume a subscription*?
No
Yes



 

Sink

 

 

lazy 後載

 

 

元件導入

import "package_name"; 
建議參考dart的官方建議規則

用到第三方元件的話,還需要修改pubspec.yaml做下載依賴的動作。
 


必用元件庫

  • dart: core :系統基本運作必須的元件
  • dart: async  :異步操作
  • dart: collection :集合操作
  • dart: convert:資料轉換(文字數值互轉,字碼處理,json處理,url編碼等等)
  • dart: developrt:debug必須的元件
  • dart: math:基本數學函式,亂數

web相關元件庫

  • dart: html:html相關操作
  • dart: svg:svg向量圖處理
  • dart: web_gl:web gl(3D)相關處理

第三方元件

  • archive:資料壓縮相關
  • http:http get / post 網路取得資料的處理
  • logging:debug / log相關
  • stack_trace:程式執行錯誤時可以取得錯誤位置等等詳細資料
  • permission_handler:管理app對系統的操作權限的取得與查詢動作。可以跨平台
  • sqflite:dart內建資料庫
  • auto_size_text:在固定長寬的範圍內自動縮放文字
  • flutter_staggered_grid_view:grid view
  • cached_network_image: 快取網路圖片
  • flutter_screenutil:管理螢幕大小與方向與文字大小的自動縮放
  • path:檔案路徑處理
  • connectivity_plus:查詢網路連線狀態
  • badges:在widgets上面顯示小圓圈跟字
  • share_plus:sns app常見的「分享」功能
  • package_info_plus:os資訊取得
  • table_calendar:月曆
  • device_info_plus:裝置資訊(手機序號等等)
  • json_serializable:json格式的generator跟parser
  • expandable:點選groupview就可以出現childview的ui
  • uni_links:讓os開啟特定url可以啟動app
  • synchronized:避免同時存取物件的race condition
  • keyboard_actions:ios鍵盤收起比較麻煩,因為沒有back鍵。這個可以方便ios / android kaypad上面的特殊功能
  • awesome_dialog:顯示對話框
  • date_format:顯示日期
  • clipboard:剪貼功能
  • upgrader:app新版提醒使用者下載
  • objectbox: NoSQL database

 

 

Flutter

總算是要進入flutter的部分了。
「效能」是flutter開發的一個重要考量。畢竟flutter的ui繪圖系統是架構在各個os的繪圖系統再上一層。多了一層,效能自然會有所損失。

 

環境設定:

安裝flutter sdk:到官網下載安裝包。解開安裝包,放在想要的磁碟路徑。此路徑就會固定為flutter sdk的路徑。


使用android arctic fox 2020.3以後的版本。啟動ide環境之後,選擇上方menu toolbar的「Preferences」->「Plugins」,選擇「Marketplace」tab,搜尋「flutter」,然後安裝。若之前沒有安裝過dart plugin,會要求安裝dart plugin。就直接同意安裝即可。



 

安裝plugin之後,android studio ide會要求重新啟動ide,就重新啟動。這時選擇「File」->「New」,就會發現多了一個選項「New flutter project」。這時需要填入之前解開的flutter sdk的路徑。

這一頁要注意的是,project命名的格式。dart要求必須全部小寫,可以有底線。另外就是不可以跟系統命名衝突。像「flutter_test」這個命名就不行...

設定完成之後,就會開啟一個開發環境的視窗,進入開發環境。
會自動打開一個範例:main.dart。

 

可以直接點擊視窗右上角的綠色三角形,執行程式。若android studio有設定好,就會執行android模擬器,並運行範例程式。視窗的右上角還有一個閃電的符號,就是flutter引以為傲的「熱重載」。可以隨時修改程式碼隨時點擊閃電符號上傳,不需重新編譯app與執行app。

這時可能會發現,在左側的檔案列表找不到main.dart放在哪。
請記得要切換左側上方的「android」改成「project」,在lib目錄裡面就可以看到。
也建議這時可以看一下External Libraries,了解有哪些基本模組可以使用。

可以看到,「flutter_test」是基本模組。難怪在命名project的時候不能使用此名。

 

 

 

 

Flutter開發的基本架構:  

  • Widget:flutter建議所有的東西都做成元件。不管是ui的視覺元件,或是無ui的資料元件,還是ui與資料混合的元件。
  • Navigator:相當於「頁面切換」(android activity)。
  • BloC:以「事件-狀態-變化」為中心理念。以app的開發來說,建議把ui頁面的轉換以此概念做設計,因為它會紀錄狀態變化,對於需要回上一步的設計很有用。
  • Future / Stream:使用者操作的輸入,網路資料的存取,資料庫的存取,都是以stream為操作方式。
  • 效率化:主要是ui的重繪方式的微調。

 

Flutter的進入點

void main() {
  runApp(const MyApp());
}

前面有提到,dart的進入點是main()。而flutter的進入點是「runApp()」。MyApp則是flutter的widget。

 

Widget(元件)

因為flutter的世界全都是widget,不管是可視或是不可視。命名規則相對重要。

整個app的根本元件,會以「xxx-app」命名。所以範例程式以「MyApp」命名。繼承了「
StatelessWidget」。

StatelessWidget與StatefulWidget的分別:StatefulWidget有提供重建方法。
若是ui元件是
該區塊內的ui元件有任何因資料變動,需要自動執行重建的需求,都建議繼承StatefulWidget。

「State」就是flutter儲存ui元件的顯示資訊的思考方式。

決定一個顯示元件是不是需要重建,在layout的越下層越好。因為上層的重建,也會讓下層的所有元件重建。每次的重建,影響的元件越少,重建就越快。

flutter的project一開始建立完成所給的範例app的根本layout,什麼都沒有,也就沒有重建的問題,所以繼承StatelessWidget。

 

決定是不是有重建需求之後,需要實作Widget方法:

  • createState():StatefulWidget的方法。需要實作一個回傳「繼承了State且使用了StatefulWidget的泛型的物件」。至於指定泛形為<MyHomePage>,是為了讓這個StatefulWidget及其下層的元件取得MyHomePage的特有參數。
    這個動作在元件被加入顯示區等等動作的時候會被執行。
  • build():StatelessWidget的方法。因為沒有重建需求,直接實作ui元件的建構動作。
    設計的時候,建議把所有變動資料獨立於元件之外,在元件重建的時候去讀取資料並顯示。 
  • State這個class包含了SetState()方法,用來通知重建。
    執行此方法也會連帶執行build(),達成重建。(不需自行call build())
  • State這個物件除了SetState()重建動作之外,也會連帶儲存該層或是下層的元件需顯示的資料。
    可以進一步做資料分層的設計。哪些資料該放上層,那些資料該放下層,就是可以考量的地方。
    像是text元件就會儲存字串,icon元件就會儲存圖片來源。
    另外,State應設計為重建的時候不會被重置,才能記錄之前的顯示狀態。

 

範例程式,在MyApp的build,宣告了「MaterialApp」。flutter提供了「MaterialApp」跟「CupertinoApp」兩種。都是繼承WidgetsApp的類別。分別表示android跟ios的ui 風格。也許可以做到同一個app,在不同的平台上面執行會有不同的ui呈現方式。

WidgetsApp需要的資料:

  • title:app的title。這個欄位因為android還有ios都有自己的設定,基本上用不到。
  • home:建立一開始的layout。
  • routes:傳入<String, WidgetBuilder>的map, 指定各個route的對應layout。
    WidgetBuilder是一個傳入BuildContext,回傳Widget的function。只要傳入具有「build()」方法的類別即可。StatelessWidget跟StatefulWidget皆可。
  • initialRoute:若是有定義許多的routelist,可以指定一開始的路徑。若不指定的話就會使用第一個定義的路徑。


接著看「_MyHomePageState」。這邊開始定義MaterialApp的第一個畫面的內容。
目前只使用MaterialApp定義了顯示的風格,任何的顯示元件都還沒有定義。

build()的第一個動作是宣告了「Scaffold」。這是一個擁有「在畫面上緣顯示的appbar / 主要顯示區 / 左右側滑區 / 右下的動作按鈕」的app頁面模板。包含了目前android使用者已經習慣的操作方式。
若是宣告為CupertinoApp的話,這裡宣告的會是「CupertinoScaffold」。命名雖然跟MaterialApp所使用的「Scaffold」接近,還是有需多差異。像是沒有右下角的ActionButton可用。當然右下角的ActionButton要在Cupertino的環境加上去也不是難事,就看是想提供怎樣的ui。
筆者之前主要是開發android app,之後會以MaterialApp的開發方式為主要的敘述方式。想要在不同的平台上面執行會有不同的ui呈現方式的兩邊兼顧的做法,在做ui設計的時候就必須考量差異點。

這邊可以做個練習:在修改MaterialApp為CupertinoApp之後,需要做怎樣的參數或是宣告修改可以讓編譯通過並顯示(先不管右下角的ActionButton是不是能出現並運作)。大約只要改5~6個地方即可。

 

layout(排版 / 定位)


flutter沒有排版描述的script,全部以dart程式碼撰寫。
排版的基本流程:依想要的排版使用定位方式元件,在定位元件的child / children放入要顯示的元件,然後依需求往外包覆動畫,後載,可視狀態的控制等等元件。

包覆的優先順序:

  • 全體資料(Theme / MediaQuery / FocusScope)
  • 操作(判定 / 輸入 / Form)
  • 定位(動畫也算是定位)
  • 顯示

官方的layout widget列表: https://docs.flutter.dev/development/ui/widgets/layout 

  • 單一顯示元件:Text / Icon / Image等等
  • 定位元件:可以容納一或多個元件。
    使用Constraints指定位置。
    共通的參數是「child」「children」,傳入一或多個widgetBuilder。
    下面是一些的舉例:
    • Container:元件需要指定背景/padding/margin/size/constraints的時候可以使用。是一個多功能的綜合元件
    • Padding:指定與所有可用空間的上下左右距離。相當於android的setMargin()。
      此功能可以使用Container解決。這個元件應該相對少用。
    • Scaffold:固定模版,填入顯示區域該建立的元件。
    • Align:使用有限選項的Constraints,指定在區域內的九宮格位置。 
      • Center:指定置中的Align。
    • Row / Column / Flex:橫向/縱向/陣列式多元件的表列。可容納多個widget,不可使用滑動的動作。
    • ListView:橫向/縱向多元件的表列。可容納多個widget,可使用滑動的動作。
    • SingleChildScrollView:相當於android的ScrollView。只能有一個child,不過下一層當然可以用多個child的項目。
    • Flex:以指定使用空間比例進行layout的方式。
      • Flexible:使用剩餘空間。使用參數「flex: n」指定長寬比例為上層元件總長總寬的1/n。
      • Expanded:使用所有的可用空間。若同一階的元件都指定為Expanded,會均分空間。其實就是Flexable的flex參數n為1的狀況。跟Flexable的差異在「child內容必須指定元件才會發生作用」
        以android的規格來說,相當於android的match_parent設定。不使用Expanded,則以warp_content的方式處理。
    • AspectRatio:保持長寬比。對於顯示圖像或是正方形很好用。
    • Intrinsic Width / Height:使用元件的固有寬度/高度進行判斷。一般的元件都是從上往下決定長寬。這兩個元件因為是從底下往上算,要是階層越深的話算得複雜也越耗資源,不要輕易使用。
    • Stack:以指定xy位置為定位方式。可重疊。越後面的會疊在越上面。類似android的FrameLayout
    • Table:類似html的<table>標籤的定位方式
    • Silver系列的元件:可以實作客製滑動操作的介面,以提供客製的滑動效果。通常跟CustomScrollView做配合使用。
    • PageView:類似android的ViewPager
    • LayoutBuilder:會提供目前階層的layout條件,作為定位參考
    • OffStage:若是該元件的資料必須存在讓其他元件參考但是又不想顯示在畫面上,就可以使用這個元件包住。
      以android的layout方式來說,就是有元件需要用到setVisibility()的話就需要它。
    • FutureBuilder / StreamBuilder:元件若是需要等待資料到來才能建立,就需要使用這兩種元件。也可以設定資料還沒到的狀態該怎麼顯示,資料錯誤該怎麼顯示。Stream的話還可以判斷資料讀取的比例。
    • Semantics:設定各種metadata。基本上是讓app若是要支援html操作使用。讓聲音閱讀器可以得知圖片內容等等附加資訊。
    • FadeTransition: 動畫效果。其他效果請參考官方元件列表。
  • 輸入元件:
    • TextField:文字輸入
    • CheckBox / Radio / IconButton / TextButton / Switch等等基礎元件,在官方網站的歸類是放在Material Components widgets裡面
    • Form:可以放入許多輸入元件。做資料的整體處理,輸入資料的格式防呆,判斷是否修改,暫存編輯資料,方便app切換時再次顯示等等。

  • 其他的只接受操作的元件,配合可視元件使用
    • AbsorbPointer:讓掛在下面的所有元件都不做擊中判斷(游標的moveon / moveoff,click,longclick)
    • Dismissable:讓掛在下面的元件具有滑動刪除的操作
    • Draggable:讓元件可拖動。
    • DragTarget:正在被拖動元件的落點。若沒有的話會直接落在操作停止的地方。有指定,但是沒有拖到指定落點的話,算是無效拖拉。無效拖拉的判定條件是「手指的觸點或是滑鼠游標是方的地方是不是在dragtarget裡面」
    • GestureDetector:指定範圍內的操作判定器
    • Focus / FocusScope:使元件 / 元件組具有被focus的功能。若是元件 / 元件組有被Focus的需求,都需要實作這個元件。
      系統提供的基本元件,只要能被focus的都已經實作了。不過要是有需要客製的focus順序調整之類的,就需要實作這部分。
    • FocusNode:讓可被focus的元件掛載。記住不要每次build都新建一個node。這樣會無法管理focus的狀態,也會導致記憶體洩漏。
  • 組合元件
    • AlertDialog / CircularProgressIndicator / Chip / DatePicker / TimePicker
  • 換頁元件
    • Navigator:頁面切換的工具。要注意的是「pushReplacement」系列指令,會覆蓋目前的路徑。也就是pop的時候,會取不到前一個被覆蓋的路徑。就依需求使用
  • 動畫元件:在動畫的執行期間,元件最好不要重建,因為相當耗資源。會使用到動畫的widget都建議設計為Stateless。會變動顯示的地方,以Animation的參數做為參考。
    • AnimatedList:使用資料「Animation<double>」串起列表widget的動畫動作。
      widget需利用這個資料進行動畫的變化。套用既有的Transition動作,或是自行設計Transition。
    • AnimatedListState:以list的方式管理各個元件進行增減,所需執行的動畫效果。可以定義加入元件與移除元件所需要執行的動畫效果。flutter建議的設計是資料的list管理跟動畫的list管理做成一個model,執行增減元件的時候,方便資料與顯示兩邊同時處理。
    • AnimateSwitcher:動畫切換不同元件的顯示
  • 其他重要元件
    • Canvas:手動繪圖
    • MethodChannel:存取os的api的方法。flutter跟os 兩邊都要對應實作。適合用在一來一回的設計。
      要注意的是,os端的「收傳動作」必須在main thread。當然可以接收指令之後開其他thread處理。
    • EventChannel:跟MethodChannel類似。不過是以Stream的方式運作。適合用在持續(不一定是連續)資料傳輸的設計。
    • AssetBundle:存取app檔案資源


Key管理

這裡的Key是指「flutter的元件管理用的id」。不是指Map的key-value,也不是指SSL的key。
為什麼需要做Key的管理,因為flutter使用「元件掛載的key」來判斷元件是否為「同一個」。


一般的元件使用並不需要設定Key。
但是有兩個以上的同類別元件,進行一個流程的時候就會需要。
像是兩個同類別的元件要進行轉場動畫,若是沒有指定key的話,既使傳入一個新建立的widget,flutter會認為沒有變動,然後就看不到資料變動的效果了。

flutter不能使用同一個實體化的元件掛在不同的顯示區。也建議Key不要每一次重建就產生一次。

當然,以上是有需要的時候才會使用。Key最好用的設計方式,就是拿來夾帶顯示資料。

ValueKey<String> vkStr = ValueKey<String>("test");  //存放泛形資料。需要傳入一個物件作為初始值。

ObjectKey ok = ObjectKey(obj); //存放物件。應該是蠻常用的。需要傳入一個物件作為初始值。

GlobalKey<T> gkt = GlobalKey<T>();  //存放泛形資料。沒有傳入參數,建立全新的物件。GlobalKey會存活於整個app內,而且唯一。

 

 

 

design pattern

  • class對外介面一律以getset設計,不要讓外部直接操作內部參數。這樣在設計State類別的setter的時候,可以很容易地使用setState()重建。

 

 

 

 

 

 

0 件のコメント:

コメントを投稿