2019年8月29日 星期四

Flutter 使用 Cloud Firestore 十步驟

1. 上 Firebase 網站註冊啟用 Firebase :    https://firebase.google.com/
2. 進入 Firebase  Console 註冊開啟新專案:    https://console.firebase.google.com/
3. 每個專案可以新增多個應用程式, 在專案裏面新增一個程式名稱(每個應用程式將會對應一個 google-services.json 檔案),  程式名稱之前要冠上 domain name, 像是:
    com.examle.appName
4. 至 Firebase console 的應用程式專頁, 下載檔案 google-services.json, 把他放到 flutter 專案目錄 /android/app 下面
5. 修改 android 專案底下檔案:  /android/build.gradle, 加入 classpath
    buildscript {
        dependencies {
            //...
           classpath 'com.google.gms:google-services:4.3.0'
        }
    }
6. 修改 android/app 應用程式底下檔案:  /android/app/build.gradle   設定應用程式名稱, 允許 multiDexEnable, 啟用 com.google.gms.google-service 的 plugin:
    android {
        defaultConfig {
            applicationId "com.example.appName"
            multiDexEnabled true
             /...
        }       
        //...
    }
   apply plugin: 'com.google.gms.google-services'
7. 至 Firebase console 的 Database 專頁啟用 Cloud FireStore 資料庫. 專有名詞 Collection 與 Document, 應用起來比較像是在使用檔案(/collection0/document0/collection1/document1/...), 用路徑 collection 與 document 穿插的方式取得所要的文件(Document), 我們不能用傳統資料庫的 Table (資料表)與 Row (紀錄)的概念去理解它, 若用資料夾與檔案的觀點來看, Collection 比較類似目錄裡面的特殊檔案夾 ./ (目前目錄)的角色, Document 則兼俱一般檔案夾與檔案的特色. 在 Database Console 下操作時, 只能在 Collection 裏面放置 Document (Collection 就是Document 的集合). 至於 Document  裏面, 可以加入其他 Document 或是另外插入下層的Collection, 當 Document 裏面插入了下層的Collection, 就能把資料向下擴展, 若是放的是 Document  他就自成一筆資料(Row), 紀錄(record) row data,也就是說可以用不同 Documents 存放不同紀錄, 當然每個 Document 要先配置好欄位 field, 每個欄位可以參考(reference)其他 Document 形成關聯式文件, 經由如此不斷合蹤與連橫, 就能相互串聯成龐大的資料庫, 例如我可以單純定義一個 Collection 作為資料表Table, 例如稱作 Book,  Document 不用特別指定名稱, 讓系統自動建立(auto id),  欄位可以定義像是 name, author, price 等等把他敲進資料表順便紀錄 row data, 若要新增多筆資料時就一一建立起不同的 Document. 或是先定義一個 Collection 作為 database, 在裏面建立不同的 Document, 在Document 裏面插入下層 Collection  稱為 Table, 在 Table 裏面再擺放其他 Documen, 該 Document 定義一些欄位 fields, 去紀錄 row data , 隨自個喜好, 只要用 Collection 加 Document 就能組合成想要的資料結構. 總而言之, 可以簡單將 Collection 視為一個節點(node), Document 就是端點(end point), 當端點要往下擴展時就必須先插入一個節點(Collection), 接著在節點(Collection)裏擺進其他端點( Doctument),成為子端點,根 / 可說是最上層的端點, 但不能放其他文件(Document), 為了往下擴展, 就必需先插入節點(Collection), 擺放不同的節點(Collection),等於往下產生不同的路徑,資料庫最終搜索的目標是端點(Document), 而節點(Collection)就集合了各種端點(Document).
8. 在 Flutter 專案目錄修改 pubspec.yaml , 加入 cloud_firestore 支援:
    dependencies:
        cloud_firestore:
9. 寫個範例程式, 利用 StreamBuilder , 讓他自動跟 Firstore 溝通, 只要一上網就會透過資料庫(Collection)更新頁面資料, 主程式 main.dart 在 lib 底下:
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
void main( ) => runApp(MaterialApp(home: MyApp( ) ) );
class MyApp extends StatefulWidget {  @override createState( ) => MyAppState( );}
class MyAppState extends State<MyApp> {
  final listWidget = <Widget>[ ];
  streamWidget( ) => StreamBuilder(
      stream  : Firestore.instance.collection('Book').snapshots( ),
      builder : (_, snapshot) { 
          listWidget.clear( );                       
          if (! snapshot.hasData) return Text("Loading ...");
          int number = snapshot.data.documents.length; // number of documments
          while(number>0) {
             final doc = snapshot.data.documents[--number];
             listWidget.add(Text("書名${doc['name']}: 作者:${doc['author']}:${doc['price']}元"));
          }
          return Column(children: listWidget);
      }
  );
  @override build(BuildContext context) => Scaffold(
      appBar: AppBar(title: Text('Cloud FireStore Example')),
      body  : streamWidget( )
  );
}
10. , 連結手機, 使用 vscode  開啟  Flutter 上述專案資料夾, 打開終端機, 驗證是否成功, 執行:
    flutter pub get
    flutter run
    
p.s. 後續如果要寫不同應用程式, 一種是上 Firebase Console 再新增產生新的 google-services.json, 下載後放入 /android/app 目錄底下, 或是用原先的 google-services.json, 此時必須修改 /android/app/build.gradle 檔案,把裏面 applicationId  改成符合  google-services.json 所設定的名稱就可,  換句話說 google-services.json 對應的是檔案 /android/app/build.gradle 裏面 applicationId 的設定值, 並非程式名稱.
       

2019年8月27日 星期二

理解 Flutter 的專有名詞 Widget 與 State

Flutter 讓人很容易短時間就能開發出 app, 關鍵在於他開發了很多種小部件稱為 Widget, 將Widget 組合起來就是一個 app. 所謂件(Widget)就是一個可以呈現在螢幕上的視覺系件,幾乎可以說 Flutter 每個物件都是件.  開發者透過件類型的建構式產生件(instance), Flutter 內的狀態機再呼叫 build 專屬方法產生件, 物件類型可以透過繼承(extends)兩種部件類型分別是靜態件類型(StatelessWidget)與動態件類型(StatefulWidget), 在專屬的方法 build 裏面客製出靜態部件動態部件, 動態件類型與靜態件類型差異在於前者產生 build 件之前先呼叫 createState 產生動態物件的狀態件(instance 簡稱物件狀態), 接著再呼叫物件專屬方法 build 產生一個動態件:
           class 靜態類型 extends  StatelessWidget { // 靜態物件 = 靜態類型( )
                    // ... 特性常數在此宣告 propertyName
                    @override  build(BuildContext _) { // Flutter 動態機呼叫後產生靜態部件
                           // 用靜態或動態件(instance)當養份
                           //... 在此客製化下層靜態物件
                          // 最後 return 靜態部件
                     }
           }
           class 動態類型 extends  StatefulWidget {  // 動態物件 = 動態類型( )
                    // ... 特性常數在此宣告 propertyName
                    @override  createState( ) => 件狀態類型( )
           }
           class  件狀態類型 extends  State<動態件類型> { //件狀態 = 件狀態類型( )
                    // ...動態變數在此宣告 stateName                   
                    @initState ( ) {
                       //一次性作動在此執行,一旦更新時可呼叫setState註冊,執行build產生新
                       // 可以透過 widget.propertyName 讀取動態物件裡面的特性 propertyName
                    }
                   @override  build(BuildContext _)  { // Flutter 動態機呼叫後產生動態部件
                       // 用靜態或動態件 instance 當養份
                       // ... 在此客製化下層 動態物件
                       // ...   一旦更新可以呼叫setState註冊, 再一次執行 build 產生新
                       // 可以透過 widget.propertyName 讀取動態物件裡面的特性 propertyName
                       // 最後 return 動態部件
                   }
          }
          class    根類型 extends StatelessWidget {  // 根物件 = 根類型 ( )
                       @override  build(BuildContext _) => MaterialApp( // 客制化根部件
                               theme : ThemeData.dark( ),  // Flutter 內部物件
                               home  :  動態物件 // 用動態物件當養份客制化其他枝葉部件 ...
                           // home  :  靜態物件 // 或用 靜態物件當養份客制化其他枝葉部件 ...
                       )
          }
          main( ) => runApp(根物件);
備註:
關於部件樹(Widget tree)的理解:
Fluter 內部所維護的一棵件樹, 是從物件(instance)中的方法(method) build 根據上下文(BuildContex)開始長出來的, 打從第一顆種子(通常是一個靜態類型的物件), 交給程序 runApp 把他發芽, 長出(build)第一個樹根, 例如可以用 MaterialApp 產生根(部件), 只要在樹根部件(方法 build 裏面)供應養份給根(部件)就能繼續客製化下層部件, 進而開枝散葉, 長出(build)新樹枝(部件), 新樹枝茁壯長出(build)葉子(部件)等等(全都比擬成部件成長事件),最後佈建出一整顆樹(佈建樹). 部件因為生長點位置(BuildContext), 自然而然形成一個有上下階層的關係(樹根 -> ...樹枝 -> ...樹葉), 至於動態部件會隨時間流逝, 狀態的改變, 隨之新陳代謝. Flutter 內部可能是以 1/60 秒執行一次狀態機(state machine)方式, 長出(build)部件,呈現(render)一個螢幕畫面.

關於狀態的理解:
螢幕以 x-y 為座標軸的平面來呈現(render)畫面,以z 軸(垂直螢幕軸)代表時間軸變化的狀態軸, 當每個畫面不斷更新時就能產生動畫, 動態物件隨著不斷的更新變數的狀態就能呈現動態畫面, 而靜態物件因為永遠不作更新,自然看起來就是靜止畫面!

還有一種部件類型是  SingleChildRenderObjectWidget, 稱為獨顯類型, 是一種可以在 Canvas 直接作畫的類型, 此類類型要先繼承自作畫類型 CustomPainter, 透過參數 painter 或是 foregroundPainter (前景作家), 傳給建構式 CustomPaint 產生部件, 後續就會呼叫裡面的方法 paint 直接作畫, CustomPaint 建構式用 size 參數(包含長與寬)設定 canvas 的長跟寬, 或是繼承自 child 參數所設定的 size(包含長與寬).

一些常用部件建構式:
Text 部件用來包裝文字串成為單一部件, 甚至加上參數還能做一些特效, 例如:
              Text("這是文字串",  style:  TextStyle(fontSize: 48) )
Row 部件用來包裝部件陣列 <Widget>[ ] 成單一部件, 參數是 children, 將會佔據螢幕一整列(螢幕橫軸 width 拉至最大), 佔據長度(蹤軸 height)依據部件列表裡面長度取最大值,  陣列裏面每個部件依序放在不同行顯示, 若是文字, 預設是擺在該行的中間. 例如:
              Row( children: [
                     Text("這是第 1 行"),
                     Text("第 2 行"),
                     Text("行")
              ])
Column 部件用來包裝部件陣列 <Widget>[ ] 成單一部件, 參數是 children, 將會來佔據螢幕一整行(螢幕蹤軸 height 拉至最大), 佔據寬度(橫軸 width)依據部件列表裡面寬度取最大值,  陣列裏面每個部件依序放在不同列顯示, 若是文字, 預設是擺在當列的中間. 例如:
               Column( children: [
                    Text("這是第 1 列"),    Text("第 2 列"),    Text("列")
               ])
Center  部件用來包裝單一部件, 參數是 child, 會將子部件擺至自由度不受限的中間, 當 child 指定用 Center 部件去包裝文字部件時, 因為子部件只是單純文字部件, 文字的 height 及 width 自由度並不設限, Center 就會儘可能往外擴展, 於是就佔據了整個螢幕(width * height)空間, 而文字部件就放在正中間, 因此要將整個螢幕當畫布可以這樣用:
         CustomPaint(
               painter: Logo( ),
               child    : Center(child: Text("Logo"))
         )
上述是先作畫, 接著貼上子部件, 換句話說就是背景作畫. 若要用前景作畫, 則需改用參數 foregroundPainter 先畫子部件, 接著在作畫蓋掉子部件(如果作畫的座標一樣的話):
         CustomPaint(child: Center(child: Text("Logo")), foregroundPainter: Logo( ))
要注意的是:如果要將 Column 部件包在  Center 裏面時, 意味是將 Column 部件擺中間而已並不會去調整裡面子部件的位置, Column 的寬度是不受限的, 因此整行看起來是放在螢幕橫軸中間, 但若將 Row 部件包在  Center 裏面時, 意味是將 Row 部件擺至中間而已,也不會去調整裡面子部件的位置, Row  的長度是不受限的, 因此整列看起來是放在螢幕蹤軸中間. 底下是完整範例:
main( ) => runApp(MyApp( ));
class MyApp extends StatelessWidget { // 靜態部件類型,  將產生根物件
      @override build(BuildContext _)  => MaterialApp(  // 根部件
           theme : ThemeData.dark( ),
           home  :  HomePage( )
     );
}
class HomePage extends StatelessWidget  { // 靜態部件類型
      @override build(BuildContext  _)  => Scaffold (  // 主幹部件
           appBar: AppBar(title: Text("關於")),
           body    : CustomPaint(painter: Logo( ),  child: Center(child: Text("這是 Logo")))
       );
}
class   Logo  extends CustomPainter { // 作畫類型
     final _pen = Paint( ) .. color  = Colors.red;
     @override shouldRepaint(Logo _) => _pen != _._pen;
     @override paint (Canvas canvas, Size size)  { // 直接用 paint 方法在 Canvas 作畫
         final center = Offset(size.width/2, size.height/2); // 畫筆至中心點
         final radius = size.width/2; // 圓半徑
         canvas.drawCircle(center,  radius, _pen); // 畫圓
     }
}
因為建構根部件時用不到 context , MaterialApp 本身是一個根部件建構式, 根物件也是物件之一, 因此可以簡單這樣用, 省去一個 StatelessWidget:
main( ) => runApp(MaterialApp(theme : ThemeData.dark(),  home  :  HomePage( ));
class HomePage extends StatelessWidget  { // 靜態部件類型
      @override build(BuildContext  _)  => Scaffold (  // 主幹部件
           appBar: AppBar(title: Text("關於")),
           body    : CustomPaint(painter: Logo( ),  child: Center(child: Text("這是 Logo")))
       );
}
class   Logo  extends CustomPainter { // 作畫類型
      final _pen = Paint( ) .. color  = Colors.red;
     @override shouldRepaint(Logo _) => _pen != _._pen;
     @override paint (Canvas canvas, Size size)  { // 直接用 paint 方法在 Canvas 作畫
         final center = Offset(size.width/2, size.height/2); // 畫筆至中心點
         final radius = size.width/2; // 圓半徑
         canvas.drawCircle(center,  radius, _pen); // 畫圓
     }
}
或者乾脆刪除 StatelessWidget,  作畫程式變得更簡單:
void main() =>  runApp(
       MaterialApp( theme : ThemeData.dark(),  home: Scaffold (      
           appBar : AppBar(title: Text("關於")),
           body     : CustomPaint(painter: Logo( ),  child: Center(child: Text("這是 Logo")))
       ))
);
class   Logo  extends CustomPainter { // 作畫類型
      final _pen = Paint( ) .. color  = Colors.red;
     @override shouldRepaint(Logo _) => _pen != _._pen;
     @override paint (Canvas canvas, Size size)  { // 直接用 paint 方法在 Canvas 作畫
         final center = Offset(size.width/2, size.height/2); // 畫筆至中心點
         final radius = size.width/2; // 圓半徑
         canvas.drawCircle(center,  radius, _pen); // 畫圓
     }
}

2019年8月24日 星期六

dart 建構式與函式參數

在Dart 語法中, 函式並不支援函式同名共存(function overloading),類型建構函式(建構式主名稱與類型名稱相同的方法 )自然也不支援構名共存(constructor overloading), 但可以用句點附名方式產生帶(副)名建構式(稱為 named constructor), 加以共存多類型建構式, 當然每個建構式名稱(主名加副名)必須唯一不得重複. 一般建構式會返回類型物件, 因此不用 return. 當建構式名稱與類型完全同名時, 就稱為內定建構式(default construct). 另外一種特殊的建構式稱為工廠建構式(factory constructor), 除了取代一般建構式外, 他必須 return 指定返回物件, 或是用來初始化子代繼承, 但返回的物件必須是該類型或是子類型的物件, 也就是說利用內部現有建構式重組後返回物件. 另外建構式的主名稱與類型的名稱必須相同. 針對 factory  的建構式還有一項特異功能甚至可以將自身物件加以包裝重組與運用, 例如讓其他程序先去處理,等結束後才返回, 或是留在永久迴圈當作主程式, 建構式經由類型名稱加上成對小括號 ( ) 就能化身產生類型物件(instance). 工廠建構式與一般建構式不同在於: 一般建構式會自行返回物件, 而工廠建構式要用 return 手動返回物件, 因為同名建構式無法並存, 因此用一般建構式或工廠建構式, 兩者只能擇一使用.

針對函式的參數, 可以用成對中括號 [ ] 作為位置可省略參數(optional position parameters), 或是用成對大括號 { } 作為帶名可省略參數(optional named parameters), 當有多個參數時, 用逗後加以分開. 所謂位置可省略參數, 顧名思義就是後續參數可以省略, 一旦指定了位置, 相對位置就是特定位置的參數. 所謂帶名可省略參數,同樣都是後續參數可以省略, 一旦要指定, 就必須用同參數名稱加冒號方式去指定該參數值, 且不受位置所影響, 若位置參數中沒用中括號或是大括號括住, 就表示輸入的位置參數是必要的, 另外參數搭配等於符號 = 可以指定預設值. 底線 _ 是一個可以當作合法命名的識別字元, 加以運用時, 若把他放在識別變數的第一個字元, 就代表著私有(private)變數的意義, 只有在該函式庫內可以使用, dart 類型中並沒有 private 或public 或 protected 區域的分類.
class Myclass {
       int a;  // 特性 a
       int b;  // 特性 b
       Myclass.a1(this.a, [this.b=0])  ;  // 一般帶名建構式  .a1
       Myclass._a1([this.a=0, this.b=0]); //另一帶名建構式 ._a1
       Myclass.b1(this.b, {this.a=0});   //另一帶名建構式  .b1
       Myclass._b1({this.a=0, this.b=0}); //另一帶名建構式 ._b1
       factory Myclass.a ( ) { // 透過帶名建構式產生物件的工廠副名建構式 .a
             return  Myclass._a1(1, 2);
       }
      factory Myclass.b ( ) {// 另一透過帶名建構式產生物件的工廠副名建構式 .b
             return  Myclass._b1(b:2,  a:1);
       }
      //  Myclass({this.a=3, this.b=4}); // 內定一般建構式完成後才產生物件
      factory   Myclass( ) { // 內定工廠建構式比較彈性, 可以針對類型物件再處理
           final obja  = Myclass.a( );  // 先產生本身物件
           final objb  = Myclass.b( );  // 還可以再一次產生物件, 甚至其他類型物件
           final obj    = Myclass._a1(obja.a, objb.b);// 物件再處理.
           runApp(obj); // 交給其他程式處理
           return obj;   //最後才返回該類型物件
      }
}
runApp(Myclass obj) {
    // ...
 }
main() {
  final   obja = Myclass.a( );   // 使用副名 .a 工廠建構式 (factory  .a constructor)
  final   objb = Myclass.b( );   // 使用副名 .b 工廠建構式 (factory .b constructor)
  final   obj    = Myclass( );     // 使用內定工廠建構式(default factory constructor)
}

dart 有兩個方便使用的方法分別是 get propertyName => _field; 及  set propertyName(value)=> _field=value; 實際上是一個仲介橋樑, 將主角(_field)隱藏起來, 或者說它是一個右進左出的方法, 讀取或寫入 propertyName 時就像一般的 property 一樣, 完全不需要加上小括號 ( ), 當寫入時用等於符號 = 指定數值:
    int  _field;  // 後台主角
    get object     => _field;  // 讀取主角的方法直接用 object
    set object(value) => _field = value; // 寫入主角的方法用 object = 指定數值
    main( ) {
         object = 3; // 寫入 3
         print(object); // 讀取 object
   }
工廠建構式(factory constructor)用帶名建構式(named constructor)搭配static (靜態物件)就可在類型內創造出一次性(唯一)共享物件(singleton), 有一點要注意的是: 靜態物件的實體, 實際上並不是放在類型裏面, compiler 最終會將它移到整體區域去初始化(就如同 c++ 靜態物件一樣, 若未指定數值內定就是 null), 宣告成靜態物件(包含方法)等同宣告帶名的整體區域物件, 因此靜態物件可以取用同樣宣告成靜態物件, 但靜態物件並不能使用 this 這個(指標)物件(因為實體不在類型裏面), 換句話說靜態方法無法取用類型裏面任何資源. 反過來, 在類型裏面卻可以讀取靜態物件, 主要原因是 compiler 已經將符號做了連結.  透過 get 方法也可方便連結. 另外 dart 語法也支援符號運算重寫(operator override) 的方法, 詳如範例:
class Singleton {  // 物件主角看似 object, 實際是 _field
    final String name; // 化身後的私名稱
    static Singleton _field;//static _field實體由該類型所有物件(instance)共享, 帶名整體區域
    get object => _field;  // 主角連結的是 Singleton._field,  帶名的整體區域物件
    get hashCode => this.hashCode ^ _field.hashCode; // 改寫 == 運算也需改寫 hashCode
    operator == (covariant other) => other is Singleton && other.object==object //運算 ==
    Singleton._(this.name);         // 帶名建構式, 副名 ._
    factory Singleton(String init) => _field == null ? Singleton._(init) :  _field;//工廠建構式
}
main( ) {
   final a = Singleton("A");
   final b = Singleton("B" );
   print(a == b);    // 因為裏面 object 宣告成 static 物件共享, 所以是同一物件
   print(a.name); // 私名稱各異
   print(b.name); // 私名稱各異
}結果:
true
A
B

簡單 c 程式碼, 根據五行八卦相生相剋推斷吉凶

#include "stdio.h" // 五行: //               木2 //      水1           火3 //         金0     土4 // // 先天八卦 vs 五行 //                    ...