2019年8月19日 星期一

用 Flutter 幾行程式搞定一個 app

如果熟悉 Javascript 與 HTML 語法, 透過 Flutter 平台, 用內建的 flutter_webview_plugin 幾行程式就能完成一個 android app, 甚至 iOS app 也應該很容易, Flutter 是用 dart 語言所開發的跨平台程式架構, 而 dart 語法跟 Javascript 長的很像, 輕鬆就能上手:
1. 使用 vscode 開啟 Flutter 新專案取名為 webview
2. 在目錄結構新增一個新目錄(document root)例如 www, 把 *.html, *.js, *.css ...等等所要用到的檔案複製到該目錄, 假設 html 主檔案是 index.html
3. 修改 pubspec.yaml 檔案, 加入 flutter_webview_plugin 並啟用 flutter_assets, 把它取名為 www/
4. 修改主程式 main.dart, 不到 10 行程式碼, 讓 WebviewScaffold 去載入 html 主檔案 index.html, 一個 app 就啟動了.
5. 就這樣, 連線到手機開啟開發者模式, 允許 debug 上傳 app, 把 app 上載到手機上執行.
p.s.
1. 修改的 pubspec.yaml 內容:
   name: webview
   description: A new Flutter project.
   version: 1.0.0+1
   environment:
        sdk: "> =2.1.0 <3.0.0"

   dependencies:
        flutter:
            sdk: flutter
        cupertino_icons: ^0.1.2
        flutter_webview_plugin: ^0.3.0+2

   dev_dependencies:
        flutter_test:
             sdk: flutter

   flutter:
       uses-material-design: true
       assets:
             -  www/


2. 修改 main.dart 的內容, 開啟 lib/main.dart 主程式, 以下內容全數複製貼過去:
import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
void main() {  runApp(MyApp()); }
class MyApp  extends StatelessWidget {
  static String assets = "file:///android_asset/flutter_assets/www";
  @override build(BuildContext context) => MaterialApp( theme:ThemeData.dark(),
    routes: {
       '/': (_) => WebviewScaffold(url:"$
assets/index.html", withZoom: true, withLocalStorage: true, withJavascript: true, allowFileURLs: true)
    }
  );

}

3. 修改 launch.json 內容, 在 vscode 拉下 Debug-> Start Debugging 或是用快速鍵 Ctrl-F5 上傳到手機上, 如果想將 app 改成 release 版, 順便減肥, 要拉下 Debug-> Open Configurations 修改 launch.json 檔案, 添加 flutterMode:
{
    "configurations": [
        {
            "name": "Flutter",
            "request": "launch",
            "type": "dart",
            "flutterMode": "release"
        }
    ]
}
4. 如果發生 WeView 找不到檔案 index.html, 可能是 Fullter 將 assets 包成 app 後所在目錄位置出錯, 可以用解壓縮程式 zip, 打開 webview/build/app/outputs/apk/release/app-release.apk, 看看到底是把 assets 目錄放在那個目錄底下, 藉此去修正程式類型 MyApp 中所定義的靜態字串 static String assets = "pathTo/www" 就能解決.

2019年8月18日 星期日

Dart 數列及資料流產生器, async/await/Future

參考 https://www.youtube.com/watch?v=TF-TBsgIErY
在 dart 世界裡, 透過 sync* 關鍵字標註一個函式讓它可以用 yield 暫停後回傳一筆資料(實際上是包裝成一個 iterable 物件直接回傳,因此不會 block 程式),讓後續用 forEach 依序讀取, 若將 yield 放進一個 for 迴圈, 就能產生一個數列:
        IntRange(int start, int end) sync* {for(int i=start; i<=end; i++)  yield i;}//一個整數數列
        void main() => IntRange(1,10).forEach(print); // 實際上是一個  iterable物件
或是搭配 yield * 直接呼叫遞迴函式(recursive funtion), 讓它看似沒有 loop 的序列產生器:
     IntRange(int start, int end) sync* { // 打包成 Iterable 類型的物件
         if (start <= end) { // 結束遞迴的條件
              yield start;      // 產生初始值
              yield * IntRange(start+1, end); // 事件(event loop)註冊, 下次繼續用遞迴函式產生
         }
     }
     void main() => IntRange(1,10).forEach(print); // 實際上是一個  iterable物件
當來到非同步世界後, 同樣可以用 yield 產生序列, 但要改用 async* (非同步產生器)標註函式, async * 所產生的是序列會包裝成資料流 stream, 而非 iterable:
     IntStrean(int start, int end) async* {for(int i=start; i<=end; i++)  yield i;}//一串整數流
     void main() => IntStrean(1,10).forEach(print);//實際上是一個 Stream<int> 物件
若是 yieild Future 物件的話, 也就是非同步串, 實際上 Future 是一個抽象類型(abstract class), 它並非當下產生資料, 而是當完成後 then( )或是等待他完成 await 才現出物件的原型:
     futureStream(int start, int end)  async * {  for(int i=start; i <=end; i++) yield     Future.value(i); } // 實際是回傳 Stream<Future>
     void main()  =>  futureStream(1,10).forEach( (f) => f.then(print));// f 實際上是 Future 物件, 用 then( ) 現出物件原型

用 yield await 看似等待完成後才回傳, 實際只是註冊到 event loop , 順便將物件打包成相對應之類型,他並不會 block 程式的運行:
     intStream(int start, int end)  async* { // 打包成 Stream 類型的物件
         for(int i=start; i <=end; i++) yield await Future.value(i); // 註冊至 event loop, 用 await 現出原型
     }
     void main() => intStream(1,10).forEach(print);//實際上是一個 Stream<int>物件
當然也能搭配 yield * 呼叫遞迴函式(recursive funtion)形成一個看似不用 loop 的資料流產生器,實際上也是註冊至 event loop, 待後續再執行:
     intStream(int start, int end) async* {
         if (start <= end) { // 結束遞迴的條件
              yield await Future.value(start);  // 註冊事件, 用 await 等待物件現出原型
              yield * intStream(start+1, end); // 只註冊至 event loop 續用遞迴函式, 無需等待
         }
     }
     void main() => intStream(1,10).forEach(print);//實際上是一個 Stream<int>物件
dart 程式庫的 Future 物件可類比成 Javascript 的 promise 物件, 而將 Future.value(值)類比成 promise 的 resolv(非同步傳回值), 他們運作邏輯是相同的, 同樣的,我們也可以用簡單邏輯來理解 async/await 的運作方式,  async 其實只是把將來要回傳(return)的值一起打包成 Future/promise 物件, 而 await 負責註冊事件並把後續程式碼(位在相應的 async 函式碼)打包放入 then( ) 的 callback 函式內,後續當物件現出原形時, 呼叫這個 callback  方法完成後續動作, then 是 Future/promise 裏面一個註冊過的從屬函式(簡稱方法), 一旦條件滿足(物件現出原形)才會執行. 對於 sync * 產生器而言, 打包的是 iterable 物件, iterable 物件是一個在迴圈中可以一個一個取出使用的一串數列, 而 async * 所打包的是 Stream 類型的物件, Stream 則是一個可以一個一個取出來的非同步事件(a sequence of events), forEach 只是其中一個取出物件的方法, 透過註冊事件與物件方法間接或直接讀取物件的原型, 而不會 block 程式的運行,如此而已. Dart 的 async/await 語法跟 Javascript 語法長的很像:
     asyncFunction( ) async =>  3; // async 標註會將函式傳回值打包成 Future 類型的物件
     void main( ) async =>  print(await asyncFunction( )); // await 只能在 async 用, 他讓物件現出原型
或是同步程式的寫法,用 Future 的 then( ) 方法現出物件原型後再處理:
     void main( )          =>  asyncFunction( ).then(print);
同步程式也可以打包成 Future 物件, 後續沿用 then 方法取得物件原型:
     syncFunction( )   => Future.value(3);  // Future.value 是立刻賦值, promise resolve
     void main( )         => syncFunction( ).then(print); // then 其實是一個 Future 物件裏面一個方法