2019年12月25日 星期三

用 android ndk 編譯 bento4

bento4 官方網站原始碼: https://www.bento4.com/downloads/
android ndk官方網站: https://developer.android.com/ndk/downloads
到上述官網下載 bento4 原始碼, 及程式編譯工具 android ndk, 分別將它解壓縮. 例如我將它解壓縮, 另外再改名, 分別放在目錄:
    export  path_to_bento4=/home/mint/Downloads/bento4
    export  path_to_ndk=/home/mint/Downloads/ndk20

1. 稍微修改 bento4 原始碼的 Ap4Config.h, (位在 $path_to_bento4/Sourece/C++/core/Ap4Config.h), 找出並修改 AP4_fseek 及 AP4_ftell 的定義:
     #define   AP4_fseek   fseek
     #define   AP4_ftell   ftell

1. 確認自己下載的 ndk 版本(最少要 24), 現在用 29, 用來設定 ANDROID_PLATFORM 環境變數, 目的是讓 AP4_fseek 及 AP4_ftell 支援 64 位元的 ftello , fseeko, 若只使用 ftell 及 fseek 則開啟檔案只能支援 32 位元長度 2^32 = 4G

2. 在 linux shell 命令列模式要先設定好 ndk 的環境變數:
    export ANDROID_NDK=$path_to_ndk
3. 切換到 betnto4 主目錄, 首先用 cmake 產生 CMakeLists.txt, 接著用 make 下指令(-j3 用 3 個 cpu 加速編譯速度), 讓 ndk 開始編譯 c 程式:
     cd  $path_to_bento4  &&  mkdir  build  &&  cd  build
     cmake  -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake   -DANDROID_PLATFORM=29   ..
     make -j3

4. 許多有用的工具程式都已編譯成 android 下可直接執行的 binary 檔, 放在目錄:
    $path_to_bento4/build
像是 mp4Fragment 可以將  mp4 轉換為 fmp4. fmp4 格式檔是一個可以透過  javascript 餵給 html5 MediaSource 播放的片段串流影音檔:
  mp4Fragment path_to_source.mp4 path_to_taget.fmp4
5.在 Android app 透過一些技巧, 例如將上述編譯完的執行檔包在 assets, 執行 app 時再將它解開,放進 app cache 目錄內. 透過 ProcessBuilder 修改成可執行權限, 就像在終端機執行一樣, http 伺服器再將轉成的 fmp4 透過網路傳給客戶端, 就成了名符其實的媒體串流伺服器 app.

2019年11月7日 星期四

實現簡單的 Motion JPEG 改用 WebSocket

1. 伺服器透過 http 連線將連線資料(html 檔案), 送給客戶端
2. 客戶端收到內含 Javascript 可以開啟 WebSocket 連線的 html 檔案
3. 伺服端收到 WebSocket 升級請求, 連線後,依照時間順序,不停將 JPEG (binary)傳給客戶端.
4. 客戶端的 Javascript, 從 WebSocket 收到的 JPEG 資料, 製成 url, 將圖貼至 canvas 就可以了
放在伺服器上的 mjpeg.html 客戶端程式範例:
<html><head><meta charset='UTF-8'></head>
<body><script>
class ViewImageWS {
    constructor(width, height) {
        this._canvas = document.createElement('canvas');
        this._canvas.width  = width;
        this._canvas.height = height;
        this._canvas.style.setProperty('border', '1px solid #000000');
        this._context = this._canvas.getContext('2d');
        this._height = this._canvas.height;
        this._width  = this._canvas.width;
        document.body.append(this._canvas);
    }
    set drawImage (_img) {
        const scaley = this._height/_img.height;
        const scalex = this._width/_img.width;
        this._context.save( );
        this._context.scale(scalex, scaley);
        this._context.drawImage(_img, 0, 0, this._width, this._height);
        this._context.restore( );
    }
    set host(_ip) {
        const ws = new WebSocket(`ws://${_ip}`);
        ws.onmessage = _jpg => {// jpg binary
            const url = URL.createObjectURL(new Blob(
                [_jpg.data],
                {type: 'image/jpeg'}
            ));
            const img = new Image( );
            const release = ( )=> {
                URL.revokeObjectURL(url);
                img.remove( );
            }
            img.style= 'transform:rotate(90deg);';// rotate 90 deg if need
            img.onerror = release;
            img.onload = ( ) => {
                this.drawImage = img;
                release( );
            };
            img.src = url;
        }
    }
};
var viewer  = new ViewImageWS(1024, 768);
viewer.host = location.origin.split('//').pop( );
</script></body>
</html>

2019年10月30日 星期三

Dart 與 Java 資料互傳


1. 陣列(array)資料結構 Dart 端稱為 Uint8List, Java 端稱為 ByteArray, 簡寫為 byte[ ]
2. 緩衝(buffer)區資料結構 Dart 端稱為 ByteData, Java 端稱為 ByteBuffer, Dart 似乎是將ByteBuffer  脫去外殼,留下裡面的 ByteArray,  用屬性 buffer 連結過去, 因此當 dart 端使用到byteData.buffer 時, 所指的內容其實就是 ByteArray. 當然還是得透過方法 Uint8List.view(byteData.buffer) 將裡面的 byteData.buffer (也就是ByteArray)轉成Uint8List 相容的資料結構.用 ByteBuffer 好處是除了可以用索引修改陣列的內容外, 也不需將陣列空間全數複製過去, 而是將連結傳送過去, 免除 copy 效率不佳的問題.
3. Dart 端透過 defaultBinaryMessenger 與 Java 端的 BinaryMessenger 用 binary 資料相互傳遞訊息, 所用就是緩衝區的資料結構來溝通, 實驗發現在 Java 端似乎只能用 ByteBuffer.allocateDirect(length) 或是 ByteBuffer.allocate(length) 分配記憶空間, 資料才能送的過去. 若是用 ByteBuffer.wrap(byte[ ]) 的方式包裝的陣列空間, 傳送似乎有問題, 詳細原因不明.
4. 若是要直接傳送原始的 binary 資料,  Java 端另外還能透過 EventChannel 將 ByteArray 直接傳到  Dart 端, 其收到的就是 Uint8List 也就是 ByteArray (byte[ ],無需再另外轉換.
5. 但若用 EventChannel 方式將 ByteBuffer.array( )當成 ByteArray 傳給 Dart, 會發現長度比原先用 allocate 分配 ByteBuffer 時的記憶空間, 整整少了 7 個位元組(bytes), 推測原因可能是 Big Endian 與 Little Endian 關係, 或是他們的資料結構儲存方式,本就不同, 尚待查證.

2019年10月24日 星期四

用 Javascript 實現簡單的 motion jpeg

伺服器上先準備好 jpeg 格式檔: mjpeg0, mjpeg1, mjpeg2  ... 等, 再寫一個 mjpeg.html 及一個 mpeg.js 檔案, mjpeg.html 範例:
<html><head><meta charset='utf-8'></head>
<body> <script type='application/javascript' src='mjpeg.js'></script></body>
</html>
底下是一個 Javascript 檔案 mjpeg.js 範例程式, 全部放到 http 伺服器同 jpeg 檔案的附錄內,  讓它執行, 用瀏覽下抓上述 mjpeg.htm, 就會定時從伺服器下載  jpeg 檔, 再繪製貼圖到 canvas, 這就是一個超簡單又名符其實的 motion jpeg 伺服器:
class MjpegViewer {
    constructor(fps, width, height) {
      this._fps     = fps;
      this._running = false;
      this._canvas  = document.createElement('canvas');
      this._canvas.width  = width;
      this._canvas.height = height;
      this._canvas.style.setProperty('border', '1px solid #000000');
      document.body.append(this._canvas);
    }
    get _duration( ){ return  0 < this._fps && this._fps < 32 ? 1000/this._fps : 100 }
    get _context( ) { return this._canvas.getContext('2d')  }    
    get stop( ) { this._running = false  }
    set host(_ip)  {
        var _counter  = 0;
        this._running = true;
        const _period = this._duration;
        const _draw = (img)=> {  this._context.drawImage(img, 0, 0) }
        const _periodRun = ( )=> {
            if(! this._running) return;
            const _img = document.createElement('img');
            _img.onerror = ( )=> { _img.remove( )  }
            _img.onload = (_)=> {
                _counter ++;
                _draw(_.target);      
                _img.remove();
            };
            _img.src = _ip + '/mjpeg' + (_counter);
            setTimeout(_periodRun, _period);
        }; 
        _periodRun( ); // fire the process
    }
};
const  viewer  = new MjpegViewer(1, 1024, 768) // fps = 1 frame/sec, canvas: 1024x768
viewer.host = 'http://192.168.0.1:8080';
setTimeout( ( )=>{ viewer.stop } , 10000);

2019年10月13日 星期日

Flutter 使用 PlatformView


vscode 開啟終端先執行以下 1 ~ 3  命令, 確認範例程式是可上傳至手機並執行的:
1. flutter create -i swift -a kotlin textview
2. cd textview
3. flutter run
接著 vscode 開啟 textview 專案夾, 修改以下檔案:
1. 開啟檔案 lib/main.dart, 將以下程式碼複製貼上:
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
  @override build(BuildContext context) => MaterialApp(
     theme: ThemeData.dark(),
     home : NdoWidgetExample('native android widget!')
  );
}
class ViewController {
  final MethodChannel _channel;
  ViewController(int id): _channel = MethodChannel('_channel$id');  
  Future<void> setText(String text) async => await _channel.invokeMethod('setText', text);
}
typedef void NdoWidgetInit(ViewController _);// function type
class NdoWidgetExample extends StatelessWidget {
  final NdoWidgetInit _init;
  NdoWidgetExample._(this._init); 
  factory NdoWidgetExample(String text) => NdoWidgetExample._(
      (ViewController _) => _.setText(text)

  );
  @override build(BuildContext context)  => Scaffold(
    appBar: AppBar(title: Text('Android TextView')),
    body  : Column(children: [Text("Flutter!"), Container(
                width: 320.0,
                height:240.0,
                color: Colors.grey,
                child: AndroidView(
                    viewType: 'ndoView',
                    onPlatformViewCreated: (int id) => _init(ViewController(id))
                )
            )])
  );
}

2.開啟檔案 android/app/src/main/kotlin/com/example/textview/MainActivity.kt, 將以下程式碼複製貼上:
package com.example.textview;
import android.os.Bundle
import io.flutter.app.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.plugin.common.PluginRegistry.Registrar
class MainActivity: FlutterActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        GeneratedPluginRegistrant.registerWith(this)
        registrarFor("com.example.textview").apply {  

          val ndoView = Factory_NdoWidget(messenger())
          platformViewRegistry().registerViewFactory("ndoView",ndoView)        
       }
    }       
}

3. 新增 android/app/src/main/kotlin/com/example/textview/TextViewPlugin.kt, 將以下程式碼複製貼上:
package com.example.textview
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.StandardMessageCodec
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory
import android.widget.TextView
import android.content.Context
class Factory_NdoWidget(private val messenger: BinaryMessenger)
: PlatformViewFactory(StandardMessageCodec.INSTANCE) {
    override fun create(context: Context, id: Int, o: Any?)
    = _NdoWidget(context, id, messenger)
}
class _NdoWidget(context: Context, id: Int, messenger: BinaryMessenger)
: PlatformView, MethodCallHandler { // interface to implement
    private val _channel = MethodChannel(messenger, "_channel$id")
    private val _view    = TextView(context)
    init {  _channel.setMethodCallHandler(this) }
    override fun getView() = _view

    override fun dispose() {   }
    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            "setText" -> {
                _view.setText(call.arguments as String);
                result.success(null)
            }
            else -> result.notImplemented()
        }
    }
}
4.重新編譯並上傳到手機執行

2019年10月8日 星期二

Flutter 與原生插件溝通

Flutter 與原生插件主要是透過通道來溝通,除了 Flutter 官網上所展示的 MethodChannel 外, 還有 EventChannel 及 MessageChannel.  他們之間的最大不同點是: EventChanel 僅能單向從原生插件端往 Flutter 平台推送資料流, 也就是說原生插件是主動發送端, Fultter 處於被動的接收資料流, 而 MethodChannel 主動權是在 Flutter 端, 原生插件處在被動回應的角色, 嚴格來說是半雙工通道(需求發送完要等待回應), 至於 MessageChannel, 視情況兩端都能各自指定為主動端或是被動端, 是一個全雙工的通道(發送與接收分開同時運作)在互相傳輸資料, 理論上來說 MessageChannel 是可以取代上述兩種通道的, 只是運作介面之不同而已.通常要馬上回應的溝通管道直接使用 MethodChannel, 需長時間的回應就就交由 EventChannel, 比方說錄影時要馬上回應機器狀態, 就直接通過 MethodChannele 溝通與控制, 當錄影開始到結束需要花費較長時間,不可能讓程序長時間等待,就可以另闢 EventChannel 管道,後續用廣播方式作通知. 用 EventChannl 與 MethodChannel 就能滿足大部份文字訊息溝通的需求, 關於 EventChannel 與 MessageChannel 的使用方式,參考範例程式:
 // 原生 flutter plugin 端 kotlin 原始碼
    import android.os.Bundle
    import io.flutter.app.FlutterActivity
    import io.flutter.plugin.common.StringCodec
    import io.flutter.plugin.common.MethodChannel
    import io.flutter.plugin.common.EventChannel
    import io.flutter.plugin.common.BasicMessageChannel
    import io.flutter.plugins.GeneratedPluginRegistrant    // ...   
    var stream : EventChannel.EventSink? = null
    EventChannel(flutterView , "com.example.event").setStreamHandler(//事件通道註冊
        object:  EventChannel.StreamHandler { // 繼承 StreamHandler 並實現
                      override fun onCancel(arguments: Any?) { stream = null  }
                      override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) {
                            stream = sink
                            stream?.success("init complete")   // 資料流非同步發送一個事件
                     }
    })
    //... async send event
    stream?.success("event !") // 資料流非同步再發送一個事件
    val msg  = BasicMessageChannel<String>(flutterView, "channel", StringCodec.INSTANCE)//簡單訊息通道註冊
    msg.send("Hello!") { reply: String? -> println(reply); } // 簡單訊息通道發送一則訊息  
    msg.setMessageHandler { req, reply ->  reply.reply("$req: plugin")  }// 同時能回應訊息
    // ...

// Flutter 端 dart 原始碼
   final _event  =  EventChannel('com.example.event'); //事件通道註冊
   _event.receiveBroadcastStream().listen((onData) {  //監聽資料流所發生的事件
         print('$onData');
   });
   final _msg = BasicMessageChannel<String>('channel', StringCodec());//簡單訊息通道   
  _msg.setMessageHandler( (String onData) async { // 簡單訊息通道等待接收訊息  
        print('receive: $onData');
        return 'hi';  // 回應資料
    });
  _msg.send('who').then(( x) =>  print('answer:$x')); // 簡單訊息通道同時能傳送訊息
總結來說, EventChannel 專用於插件的廣播頻道, MethodChannel 專用於插件的回應需求通道, 而 MessageChannel 則是一個通用的全雙工溝通管道.
使用 BinaryMessenger 通道傳輸, 參考文章:
        https://medium.com/flutter/flutter-platform-channels-ce7f540a104e
底下使用 android 端的 Kotlin 原始碼:  android platform -> flutter 溝通:
    import android.os.Bundle
    import io.flutter.app.FlutterActivity
    io.flutter.plugin.common.BinaryMessenger
    import io.flutter.plugin.common.StringCodec
    import java.nio.ByteBuffer
    import java.nio.ByteOrder
    // import io.flutter.plugin.common.BinaryCodec
    // import io.flutter.plugin.common.StandardMessageCodec
    // 若要接收監聽處理範例:
    flutterView.setMessageHandler("binary")  { buffer: ByteBuffer?,  reply ->    
      buffer?.order(ByteOrder.nativeOrder())      
      buffer?.putDouble(3.1415)
      buffer?.putInt(123456789)
      reply.reply(buffer)
    }
   // 若要傳送並等待處理範例
    val buffer = ByteBuffer.allocateDirect(12)
    buffer.putDouble(3.1415)
    buffer.putInt(123456789)  
    flutterView.send("binary", buffer) { _ -> } // 忽略回傳值
    // {  x: ByteBuffer? -> val y =  BinaryCodec.INSTANCE.decodeMessage(x) }//處理回傳值

底下使用 Flutter 端的 dart 原始碼:  flutter  -> android platform 溝通:
    // 若要接收監聽處理範例:
    BinaryMessages.setMessageHandler('binary', (ByteData buffer) async {
      final readBuffer = ReadBuffer(buffer);
      final x = readBuffer.getFloat64( );
      final n = readBuffer.getInt32( );
      print('Received $x and $n');
      return null;
    });
    // 若要傳送並等待處理範例
    final writeBuffer = WriteBuffer( ) ..
                      putFloat64(3.1415) ..
                      putInt32(12345678);
    final buffer = writeBuffer.done();
    await BinaryMessages.send('binary', buffer);
上述 BinaryMessages 通道若是透過 StringCodec( ), 就能用字串加以溝通:
    // 若要接收監聽處理範例:
    final codec = StringCodec( );
    BinaryMessages.setMessageHandler('binary', (ByteData buffer) async {
      print('${codec.decodeMessage(buffer)}');
      return codec.encodeMessage('Hi');
    });
    // 若要傳送並等待處理範例:
    final reply = await BinaryMessages.send('binary', codec.encodeMessage('Hello'))
    print(codec.decodeMessage(reply));

2019年10月2日 星期三

Flutter 透過高階函式(high orfer function)溝通 Widget

當參數帶有迴調函式時稱為高階函式,  StatefulWidget 與下層 Widget 溝通就可透過建構式傳遞高階函式方式當參數, 架起溝通管道, 底下 MyButton 雖然是一個 StatelessWidget 創造的 widget, 透過技巧可讓它看起來多彩多姿, 範例程式:
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> {
  final colors = <Color>[Colors.red, Colors.orange, Colors.yellow, Colors.green, Colors.blue, Colors.cyan,Colors.purple];
  String  title = '按下按鍵更改標題';
  int     index = 0;  // 顏色索引
  Color color( ) { // callback function to send color
    index = (index +1) % colors.length;
    return colors[index];
  }
  void changeTitle(String _) {  // callback function to change title
    setState( ( ) => title =_ ); // setState to rebuild Scaffold widget
  }
  @override build(BuildContext context) => Scaffold(
      appBar: AppBar(title: Center(child:Text(title))),
      body  : Center(child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[MyButton(color, changeTitle)],
      ))
  ); 
}
class MyButton extends StatelessWidget {
  final Color Function( ) colorIn;
  final void Function(String) titleOut;
  MyButton(this.colorIn, this.titleOut);
  @override build(BuildContext context) => FlatButton.icon(
      onPressed : ( ) => titleOut('這是新標題'),
      color     : colorIn(), // get color
      label     : Text('按下按鍵更改顏色'), 
      icon      : Icon(Icons.color_lens)
  ); 
}

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 看結果)

用 python 解簡單的常微分方程式

# sudo apt install python3-pip # python3 -m venv venv # cd venv # . bin/activate # pip3 install numpy matplotlib import numpy as np import m...