2019年9月28日 星期六

flutter 用 StreamBuilder 產生動態部件

StreamBuilder與FutureBuilder都是一個未來事件驅動的部件,他們都源自於 StatefulWidget, 因此他是一個動態部件, Future 是一次性的事件, 而 Stream 則是源源不斷的事件, 因此當未來只要完成一件事情時, 就簡單用 FutureBuilder, 若是處理源源不斷的資料流就要用 StreamBuilder. FutureBuilder 只有在事件完成時才創造出新部件,而 StreamBuild 在資料流發生事件時就創造出新部件,不用再另外寫一個繼承 StatefulWidget 的類型, 他們是由 Flutter 系統內監聽未來事件或是資料流做出反應創造出新部件, 比較特別的是StreamBuilder 與 FutureBuilder 是一種獨立於時間狀態的部件, 也不能用 setState 去改變狀態, 相較於 StatefulWidget 的部件, 同步於系統的時間狀態(最快大約是 1/60 秒的更新速率)必須用 setState 去同步系統作後續的部件更新運作截然不同, 但都具有非同步操作的特性,但要注意的是, 當要處理像是使用 Timer 之類的物件, 就只能用 StatefulWidget 去運作. 用 StreamBuilder 或是 FutureBuilder 創造動態部件的好處在於可以跟其他的部件在同一階層下享用物件內的資源來重建新部件, 就算放在靜態物件內也不失其獨立運作的動態本性,  StatefulWidget 是一個獨立的動態物件類型, 只能先透過建構式傳進資料, 例如餵進 callback Function 或是資料流建構出一個溝通管道, 後續再透過這些管道加以溝通,不像StreamBuilfer 或是 FutureBuilder 那麼簡單直接, StreamBuilder 應該是一種 BLoC 運作模式,可以搜索參考網路上更多關於 BLoC(Business Logic Component)的文章, 將運作邏輯與物件狀態分離的一種機制, 當程式內的部件過多, 或是部件之間交互作用太過複雜時, 就需理解出一套思考邏輯用於程式維護,但同時也很可能犯下一些常見的錯誤程式編寫方式, 例如宣告成整體區域變數作為溝通媒介, 造成程式邏輯雖然沒錯, 但畫面卻沒同步更新的狀況, 主要原因是變數分離後, 導致 setState 與要重建的部件不同步, 變數雖然改變了, 但畫面卻看不到, 詳述可參考 youtube 的影片:
https://www.youtube.com/watch?v=RS36gBEp8OI
// dart 模組程式
import 'package:flutter/material.dart';
mixin AAAA { // 當要混入 StatelessWidget 時, 特性常數必須 final, 讓同階層的部件共享資源
    final  a =3; // 公共變數
    final _style = TextStyle(fontSize: 24.0); // _  底線用意是同一模組下該變數可視,私有變數
    TextStyle  style( )  => _style;// 或是透過不含底線的方法 style( ) 將它公開
    void get dump => print(a); //  透過 getter 方法, 就不需後綴添加 ( )
}
void main(){      runApp(MyApp());    }
class MyApp extends StatelessWidget {// 靜態根部件
  @override Widget build(BuildContext context) => MaterialApp(
    debugShowCheckedModeBanner: false,
    theme : ThemeData.dark(),
    home  : ExamplePage()
  );
}
class ExamplePage extends StatelessWidget with AAAA {//第1層靜態部件,混入 AAAA 類型
  @override  Widget build(BuildContext context) => Scaffold(
     //內含  appBar 與 body 兩個部件, 用 StreamBuilder 創造出動態部件包進 AppBar 的title
      appBar: AppBar(title:  StreamBuilder(initialData: 0, // 初始值, 監聽資料流
                stream  : Stream<int>.periodic(Duration(milliseconds: 500), (_)=>_).take(10),
                builder : (BuildContext context, AsyncSnapshot<int> snapshot) {
                  final data = snapshot.data;
                  switch(snapshot.connectionState) {
                    case ConnectionState.waiting: return Text('Wait...', style: _style);//初始部件
                    // case ConnectionState.done : return Text('Finish.', style:_style);//私有資源
                    // case ConnectionState.none:
                    // case ConnectionState.active:
                    default: return Text('StreamBuilder $data + mixin $a', style: _style);//更新部件
                  }                 
                }
              )),
      body : Text('mixin: $a', style:_style) // 靜態部件, 取用公共資源 a, 與私有資源 _style
  );
}

2019年9月26日 星期四

Flutter 基礎, 掌握首要關鍵

1. StatelessWidget
2. StatefulWidget
3. InheritedWidget
4. StreamController
5. mixin
6. StreamBuilder 與 FutureBuilder
以上觀念了解透徹後, 稍俱 Javascript 觀念, 轉成 dart 的語法與運作邏輯, 就能得心應手.
只要跟時間相關, 會活動的部件就要繼承 extends StatefulWidget, 活動部件透過方法 setState((){}) 要求系統將部件重建, 至於靜態部件繼承 extends StatelessWidget, 若要重建, 只能透過活動式部件間接去重建一個新的, 當然前提是此部件必須是活動部件的下層部件, 因此部件之間有著上下階層關係,當上層部件重建時,下層部件就會跟進. 因此部件的佈屬方式與運作效能息息相關.一個部件當有需要時才重建, 是一個 Flutter 經典程式所需奉為圭臬的首要任務.部件透過建構式帶進參數去客製化只能單向往下溝通. 跨階層的溝通, 簡單的可經由 InheritedWidget 把參數往下遞延至子部件, 或是應用類型內的整體變數相互溝通, 複雜一點的, 可透過 StreamController 建構出管道(stream pipe)相互交流, StreamController( ) 的 stream 用來監聽 listen 輸入端進來的資料流(input stream), 而 sink 可以餵給輸出端加入 add 資料流(output stream). 把所有溝通管到全集中到 mixin 類型內,就能方便管理與運用, mixin 是一個不帶建構式的混合類型, 或者看成是方法與特性的集合營, 通常用在活動部件的狀態類型, (動態物件類型實際上也是一種靜態的類型, 因為特性常數必須 final), 當混入的各種特性都是常數時, 也許可以混進靜態類型內, 先繼承 extends 後, 接續 with 混合類型, 把方法與變數等各種特性混進來. 初始化物件可以放在部件的初始程序內, 靜態部件就直接放在建構式內, 活動式的部件除了可以放在建構式外, 有些時候要放在 initState 程序內, 部件最後是從 build 方法建構出來的, 因此 build 程式碼要愈短愈好.
備註:
1.  靜態物件與與靜態部件實際上是兩種不同的物件(instance),  同理, 活動物件與活動部件也是, 靜態物件或是活動物件(instance)是建構式產生的,  但部件(instance)確是 Flutter 透過物件內方法 built 建構出來的.
2. 靜態物件與靜態部件同在一個類型裏面, 因此特性或方法可以分享給部件:
class   My0Widget   extends   StatelessWidget  {  // 靜態物件類型, 繼承自靜態類型
       final value = 0;  // 常數必須 final , 讓靜態部件分享
      @override build(BuildContext   context)  => Text('Hello0') // 靜態部件的建構程序
}
3. 活動部件狀態變數位於活動狀態類型內, 與活動物件分屬不同的類型, 基本上, 活動物件類型內的特性都是常數(否則編譯時會有警告訊息, 只有活動狀態的物件可以是變數), 活動部件透過 widget. 可以取得活動物件內的特性, 但活動物件要反向取得活動部件的狀態, 還需要用點技巧才能取得:
imoprt 'dart:async';
import 'package:flutter/material.dart';
class   MyWidget   extends   StatefulWidget  { // 活動物件類型, 繼承自活動類型
      final  value = 0 ;     //特性常數(value) 必須 final, 活動部件透過  widget.特性常數  取值
      final  _MyState   myWidget; // 如有需要, 透過  .myWidget.狀態變數   間接取得
      static  MyWidget   singleton;  // 宣告一次性靜態共享物件, 實體是位於整體區域
      MyWidget._(this.myWidget);// 用帶名建構式, 初始化部件狀態 myWidget
      factory MyWidget( ){ // 用工廠建構式, 一次性初始化 singleton 與 _myWidget
           if( singleton == null )   singleton = MyWidget._(_MyState( )); // 初始化的活動物件
           return singleton; // 回傳靜態共享物件(instance)
      }
      @override createState( )  =>  myWidget; // 直接回傳部件狀態
}
_MyState   extends   State<MyWidget> { // 活動部件狀態類型繼承自活動狀態類型
     int   myState = 0;     // 狀態變數, 可以更新
     void rebuild(int _)  => setState(  ( ) =>  myState=_ ); // 改變狀態並呼叫部件重建的程序
    @override void initState( ) {
          Timer.periodic(Duration(seconds: 1),(_) => rebuild( myState+1) );//定時器更新狀態
     } // 活動部件的初始化程序
    @override build(BuildContext   context)  =>
      Text('Hi: myState=$myState, value=${widget.value}') ; // 活動部件的建構程序
}
// void main( ) {  print(  MyWidget( ).myWidget.myState  );   }

2019年9月22日 星期日

Flutter plugin 讀取電池電量範例

參考文章: https://flutter.dev/docs/development/platform-integration/platform-channels
1.在 vscode 開啟終端機執行以下命令, 用 kotlin 寫 android plugin 程式碼:
     flutter create -i swift -a kotlin batterylevel
2.在 vscode 開啟專案檔案夾 batterylevel, 將以下程式碼貼到 main.dart(位在lib/main.dart)
// main.dart
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
  @override build(BuildContext context) => MaterialApp(

     theme: ThemeData.dark(), 
     home: MyHomePage()
  );
}
class MyHomePage extends StatefulWidget {
  @override createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>  {
  String _battery = '';
  final _plugin = MethodChannel('com.example.batterylevel/battery');
  void _batteryUpdate(_) =>  setState(() => _battery = '$_');
  void showLevel() => _plugin.invokeMethod('getBatteryLevel').then(_batteryUpdate);
  @override void initState() {
   super.initState();

   Timer.periodic(Duration(seconds: 5), (timer) =>
     _plugin.invokeMethod('getBatteryLevel').then(_batteryUpdate)
   );
  }
  @override build(BuildContext context) => Scaffold(
      appBar: AppBar(title: Text('電池電量: $_battery %')),
      body  : Text('電池按一下,更新電量顯示'),    
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.battery_unknown),
        onPressed: showLevel
      )
  );
}

3. 編輯位在專案夾內檔案 batterylevel/android/app/src/main/kotlin/com/example/batterylevel/MainActivity.kt, 將以下程式碼複製並貼上:
// MainActivity.kt:
package com.example.batterylevel
import android.os.Bundle
import io.flutter.app.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.plugin.common.MethodChannel
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
class MainActivity: FlutterActivity() {
  private val CHANNEL = "com.example.batterylevel/battery"
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    GeneratedPluginRegistrant.registerWith(this)
    MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
      when(call.method) {
        "getBatteryLevel" -> {
          val batteryLevel = if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
            val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager

batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
          } else {
            val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
            intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
          }
          if (batteryLevel == -1) result.error("UNAVAILABLE", "電量無效.", null)
          else                    result.success(batteryLevel)        
        }
        else -> result.notImplemented()
      }
    }
  }
}

4. 編譯並上傳至手機:
   flutter run
 

2019年9月18日 星期三

dart to js Promise

dart 可以透過 dart2js 將程式碼轉換成 Javascript:
          dart2js    source.dart   -o      source.js
如果用不到 source map 檔案 (source.js.map), 可以進一步產生優化的輸出檔:
          dart2js    source.dart   -o      source.js   --no-source-maps    -O2
在 Javascript 的語法中, 實現非同步程式運作可以用 async/awiat 或是用 Promise 的  then 回調函式(callback function)去操作將來的傳回值, 用 async/await 程式碼比較乾淨,也容易理解, 但有些時候還是免不了要用 Promise 的方式(resolve or reject)去完成, dart 的類型Completer 物件就是一個類 Promise 物件,  裡面的 complete 方法可看成是 Promise 的 resolve, 而 completeError 自然就是 Promise 的 reject 方法, 裡面的 future 就是一個 Promise 物件, 換句話說 Completer 實際上是將 Promise 包裝成一個獨立物件, 因此可以這樣用:
       import 'dart:async';
       void setTimeout(callback, int mS) => Timer(Duration(milliseconds: mS),  callback);
       void main ( ) async {
            final c = Completer( );
            setTimeout( ( ) => c.complete("Done!"), 500); // compete after 500mS
            try {
                final result = await  c.future;
                print('OK:$result');
           } catch (error) {
                print('<$error>');
           }
       }
如果這樣子用不習慣, 可以採用熟悉的 resolve 與 reject, 先包裝成一個原型後, 再加以實現:
     import 'dart:async';
     abstract  class   _Promise { // 原型, 介面類型
            final field;
            resolve(_)  => this.field.complete(_);
            reject(_)     => this.field.completeError('error:$_');
            call ( )        => this.field.future; // functor to return future
            _Promise(this.field); // 建構式
     }
     class  Promise  extends  _Promise { // 繼承原型加以實現, 用 Completer 去初始化原型
               Promise( ) : super( Completer( ) ); 
     }
 也可以用工廠建構式搭配帶名建構式直接實現 Promise 類型:
     import 'dart:async';
     class Promise {
           final field;
           resolve(_)  => this.field.complete(_);
           reject(_)     => this.field.completeError('error:$_');
           call ( )        => this.field.future; // functor to return future
           Promise._(this.field); // 帶名建構式
           factory  Promise( ) => Promise._( Completer( ) );  // 用工廠建構式傳回物件, Completer( ) 初始化 this.field
     }
其實可以更簡單一點, 直接初始化 field = Completer( ), 再把需要用到 method 接起來:
     import 'dart:async';
     class Promise {
           final field = Completer( );
           resolve(_)  => this.field.complete(_);
           reject(_)     => this.field.completeError('error:$_');
           call ( )        => this.field.future; // functor to return future
     }
     void setTimeout(callback, int mS) => Timer(Duration(milliseconds: mS),  callback);
     void main ( ) async {
            final c = Promise( );
            setTimeout( ( ) => c.resolve("Done!"), 500); // resolve after 500mS
            try {
                final result = await  c( ); // 呼叫  call 函式回傳 future, 等待完成
                print('OK:$result');
           } catch (error) {
                print('<$error>');
           }
     }
顯而易見, 上述方式雖然達到了目的,  但經過物件層層堆疊, 執行效率必然變差了.
dart 語言不但能轉換成 Javascript, 經由 import 'dart:js' 的函式庫後, 透過 context  與中介方法 callMethod 就能呼叫 Javascript 原始碼, 以 console.log 呼叫為例, 將它包裝成一個類型方便使用:
     // test.dart
     import 'dart:js';
     class JS {
            final field;
            JS._(this.field);
            factory JS([String _='']) => _.length>0 ? JS._(context[_]) : JS._(context);
            void log(String s)   => this.field.callMethod('log', [s]);// 參數必須放在 List [ ] 裏面
            call (String method, String s)=> this.field.callMethod(method, [s]);// functor
     }
     void main( ) {
          final console = JS('console');// 初始化 context
          console.log( "Hello");    // 透過 log 方法
          console('log',   'Hello2');// 透過 functor 呼叫 .log
     }
用 dart2js 編譯轉換成 Javascript:
      dart2js   test.dart   -o test.js    -O2   --no-source-maps
上述透過 dart2js 編譯過的程式碼 test.js 只能在瀏覽器內執行, 因此還要寫一個 html檔案:
<html><head><meta charset='utf-8'></head><body>
    <script type='application/javascript' src='test.js'></script>
</body></html>
將以上內容存檔成 index.html, 瀏覽器開啟 index.html 就能執行(在 Chrome 相容性比較好, 打開開發工具, 點選 console 看結果)

2019年9月3日 星期二

用 Firebase cloud FireStore 建立族譜

Firestore 的路徑必須用 collection/document ... 互相穿插的命名方式, 雖然奇怪, 卻很適合用來撰寫族譜, 例如將祖先姓名當 id 命名 Documnet, 後續用 n 世代命名插入 Collection. Collection 用數字或英文字母當 id 來命名,好處是順便當作索引依據, 另一方面當要在 Firebase console 刪除整個 Collection 時比較方便, 因為刪除 Collection 要做確認的動作, 系統會要求輸入正確 id 才可刪除, 至於刪除 Document 就不用. 祖先姓名本身就是一組識別碼, 剛好用作 Document id, 通常為了資料庫管理方便, 可以將族譜統一放在根目錄的 Genealogy 底下, 因為根目錄都是以 Collection 為起點, 因此插入一個 Collection 到根路徑 / 內, 例如 Genealogy , 後續再放一個 Document, 可隨喜好自行命名, 例如可以用姓氏當作 Document, 接著再插入 Collection, 加入 Document ..., 整個路徑大概長的像這樣:
        /族譜                       /0世代             /1世代              /2世代         /3世代
實際上是:
        /Genealogy/姓氏      /0/祖先姓名     /1/長男名           /2/長男名    /3/長男名   ...
                                                           /1/次男名           /2/長男名  ...
                                                           /1/參男名
上面輸入的英文字或數字就是插入的 Collection, 輸入的中文名就是加入的 Document. 或是乾脆直接掛到根目錄底下, 直接用 Collection 0 當起點, 路徑變短一點, 隨個人喜好而定:
        /0/祖先姓名     /1/長男名           /2/長男名    /3/長男名   ...
                             /1/次男名           /2/長男名  ...
                             /1/參男名
Firebase 資料庫一旦建立, 在 console 底下操作刪除有點麻煩, 目前還不曉得如何將Collection 或 Document 般移或重新命名, 可能要用程式處理, 因此事先最好做好族譜的編排方式(特別是資料型態到底是數字還是字串), 避免重新輸入做白工.

2019年9月2日 星期一

Javascrip 應用 Promise 實現一個簡單的 mutex

通常登入帳戶後, context 不能被切換, 萬一執行非同步工作(async function)時, 有可能切換到其它帳戶, 造成 contex 大亂, 解決方式是實現一個 mutual exclusion 機制, 保護 context 順利進行:
// mutexLogin.js
class   Guest {
    constructor(id=0) { this.user = "anonymous" + id  }
}
class   Field {
    constructor(value=false) { this.field = value }
    set  value(v) { this.field = v      }
    get  value( ) { return this.field   }
}
class   Mutex   extends   Field {
    constructor( ) { super();   this.full = false;  }
    get  isLock ( ) { return this.value   } // alias getter
    get  lock   ( ) { this.value = true   } // getter, to set field
    get  unlock ( ) { this.resolve && this.resolve(true) } // getter, to resolver with true
    get  waitUnlock( ) { // getter, to return null when full, or a promise if available.
        if (this.full)  return  null;
        this.full = true; // one promise only, to skip the others.
        return  new Promise( (fullfill, x) => { // a promise
            this.resolve = (value) => { // to define resolver
                this.full  = false;
                this.value = false;
                fullfill(value) ; // to fullfill the promise
            } // end of this.resolve
        }); // end of Promise
    } // end of waitUnlock
} // end of class Mutex
const mutex = new Mutex( );
async   function   anonymousLogin(id) {
    if (mutex.isLock) {
        const  promise = mutex.waitUnlock;
        if (promise == null)   console.log("    -> skip contex for id=" + id);
        else  promise.then( (_) => {
                console.log("Wait so long: "+_);
                anonymousLogin(id); // function recursive to restart again
        });   
    } else {
        const guest = new Guest(id);
        mutex.lock;    // contex to protect
            console.log(guest.user+":");
            await doSomething(); // do some aync task
        mutex.unlock;  // end of contex
    }
}
async   function   doSomething( ) { // async task
    console.log("I am working ...");
    console.log("Done");
}
for(i =0; i<10; i++)   anonymousLogin(i);
執行  js   mutexLogin.js  看結果:
anonymous0:
I am working ...
Done
    -> skip contex for id=2
    -> skip contex for id=3
    -> skip contex for id=4
    -> skip contex for id=5
    -> skip contex for id=6
    -> skip contex for id=7
    -> skip contex for id=8
    -> skip contex for id=9
Wait so long: true
anonymous1:
I am working ...
Done

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

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