如果熟悉 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月19日 星期一
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 物件裏面一個方法
在 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 物件裏面一個方法
2019年7月28日 星期日
理解 Kotlin 的 companion object (變身) vs instance of class (化身) 之不同
在 Kotlin 中可以將類型與類型建構式分別指定給變數, 到底有何不同, 看以下例子:
class AAA { // 類型 AAA, 預設的建構式就是 AAA( )
var b = 3 // 這是待化身的變數宣告, 透過建構式 AAA( ) 化身成物件成員(instance member)
companion object {// 這是協同物件, 也就是 AAA 的變身, 唯一實體共享物件(static object)
var a = 5 // 需透過 AAA 存取成員
var b = 10 // 名字同化身的成員, 直接變身成物件成員, 必需透過 AAA 存取成員
}
}
fun main(args: Array < String > ) {
val c = AAA // c 是 AAA 的變身(companion object), 但不是化身(instance of class)
val i = AAA( ) // i 才是類型的化身, 透過類型建構式 ( ) 才能化身成物件(instance of class)
println(c.a) // a 是 AAA 共享成員
println(c.b) // b 也是 AAA 成員之一
println(i@AAA.a) // i 要透過變身標籤 @AAA 才能存取共享成員 a
println(i@AAA.b) // i 要透過變身標籤 @AAA 才能存取共享成員 b
// println(i.a) // 錯誤的用法: a 實際並非 i 的成員
// println(i.b) // 錯誤的用法: b 實際並非 i 的成員
println(i.b) // 這裡的 b 是透過建構式生成的, 並非變身(companion object)裡面的 b
執行結果:
5
10
5
10
3
變身與化身中文意義是相同的, 如果把英文的 companion object(協同物件)翻譯成"變身", 而將 instance of class (類型物件)翻譯成"化身"時, 兩者本質上都是物件, 就要區分它們的不同, 也許可以這樣理解: 一旦宣告了協同物件(簡稱變身), 類型名稱可以變身(ailias of companion object)成物件去存取共享成員, 而類型物件(instance of class)必需透過建構式去化身, 兩者物件的生成方式不同在於: 變身在是在編譯階段(compile phase)生成的靜態物件(static object),從程式開始執行到結束生命都一直維係著不會消失, 但化身是在執行階段(runtime)才生成的區域物件(scope object), 生命周期受到控制, 一旦離開所在的生命區域(scope)便消失殆盡. 變身主要目的是維繫共享資源, 實際上只有一份變身物件存在類型內, 所有類型的化身並沒有變身這個物件!如果真要在化身物件方便取用,除了用 @標籤外, 也可以用 set() 及 get() 刻意連結起來,只不過要注意的是, 因為是共享的(static object),若在多工環境下也許要用 semaphore 來加以鎖定.
class AAA { // 類型 AAA, 預設化身的建構式就是 AAA( )
companion object {// 這是協同物件, 也就是 AAA 的變身, 唯一實體共享物件(static object)
var a = 5 // AAA 變身的成員
var b = 10 // 若名稱與化身的成員相同時, 必需透過 AAA 存取成員
}
var b: Int get( ) { return AAA.b } // 符號讀取連結, 讓化身間接讀取變身成員 b
set(v: Int) { AAA.b = v } // 符號寫入連結, 讓化身間接寫入變身成員 b
fun a( ) { // 當化身成員名稱與變身的成員不同時, 化身可以取用到變身的成員只是因為內定(default)符號聯結的關係, 並不表示他就是化身的成員
return a // 等同取用 AAA.a , 只是因為 kotlin 內定會將符號 a 聯結到變身成員 AAA.a
}
}
fun main(args: Array < String > ) {
val c = AAA // c 是 AAA 的變身(companion object), 但不是化身(instance of class)
val i = AAA( ) // i 是類型的化身, 透過類型建構式 ( ) 化身成物件(instance of class)
println(c.a) // a 是 AAA 共享成員
println(c.b) // b 也是 AAA 成員之一
println(i.a()) // 呼叫函式 a( )間接讀取 AAA.a
println(i.b) // 已經刻意將 b 做了符號讀取連結, 看似直接(實際是間接)讀取
i.b = 3 // 已經刻意將 b 做了符號寫入連結, 看似直接(實際是間接)寫入
println(c.b) // 因為 b 是共享成員, 所以變身的成員也跟著改變
}
結果:
5
10
5
10
3
class AAA { // 類型 AAA, 預設的建構式就是 AAA( )
var b = 3 // 這是待化身的變數宣告, 透過建構式 AAA( ) 化身成物件成員(instance member)
companion object {// 這是協同物件, 也就是 AAA 的變身, 唯一實體共享物件(static object)
var a = 5 // 需透過 AAA 存取成員
var b = 10 // 名字同化身的成員, 直接變身成物件成員, 必需透過 AAA 存取成員
}
}
fun main(args: Array < String > ) {
val c = AAA // c 是 AAA 的變身(companion object), 但不是化身(instance of class)
val i = AAA( ) // i 才是類型的化身, 透過類型建構式 ( ) 才能化身成物件(instance of class)
println(c.a) // a 是 AAA 共享成員
println(c.b) // b 也是 AAA 成員之一
println(i@AAA.a) // i 要透過變身標籤 @AAA 才能存取共享成員 a
println(i@AAA.b) // i 要透過變身標籤 @AAA 才能存取共享成員 b
// println(i.a) // 錯誤的用法: a 實際並非 i 的成員
// println(i.b) // 錯誤的用法: b 實際並非 i 的成員
println(i.b) // 這裡的 b 是透過建構式生成的, 並非變身(companion object)裡面的 b
執行結果:
5
10
5
10
3
變身與化身中文意義是相同的, 如果把英文的 companion object(協同物件)翻譯成"變身", 而將 instance of class (類型物件)翻譯成"化身"時, 兩者本質上都是物件, 就要區分它們的不同, 也許可以這樣理解: 一旦宣告了協同物件(簡稱變身), 類型名稱可以變身(ailias of companion object)成物件去存取共享成員, 而類型物件(instance of class)必需透過建構式去化身, 兩者物件的生成方式不同在於: 變身在是在編譯階段(compile phase)生成的靜態物件(static object),從程式開始執行到結束生命都一直維係著不會消失, 但化身是在執行階段(runtime)才生成的區域物件(scope object), 生命周期受到控制, 一旦離開所在的生命區域(scope)便消失殆盡. 變身主要目的是維繫共享資源, 實際上只有一份變身物件存在類型內, 所有類型的化身並沒有變身這個物件!如果真要在化身物件方便取用,除了用 @標籤外, 也可以用 set() 及 get() 刻意連結起來,只不過要注意的是, 因為是共享的(static object),若在多工環境下也許要用 semaphore 來加以鎖定.
class AAA { // 類型 AAA, 預設化身的建構式就是 AAA( )
companion object {// 這是協同物件, 也就是 AAA 的變身, 唯一實體共享物件(static object)
var a = 5 // AAA 變身的成員
var b = 10 // 若名稱與化身的成員相同時, 必需透過 AAA 存取成員
}
var b: Int get( ) { return AAA.b } // 符號讀取連結, 讓化身間接讀取變身成員 b
set(v: Int) { AAA.b = v } // 符號寫入連結, 讓化身間接寫入變身成員 b
fun a( ) { // 當化身成員名稱與變身的成員不同時, 化身可以取用到變身的成員只是因為內定(default)符號聯結的關係, 並不表示他就是化身的成員
return a // 等同取用 AAA.a , 只是因為 kotlin 內定會將符號 a 聯結到變身成員 AAA.a
}
}
fun main(args: Array < String > ) {
val c = AAA // c 是 AAA 的變身(companion object), 但不是化身(instance of class)
val i = AAA( ) // i 是類型的化身, 透過類型建構式 ( ) 化身成物件(instance of class)
println(c.a) // a 是 AAA 共享成員
println(c.b) // b 也是 AAA 成員之一
println(i.a()) // 呼叫函式 a( )間接讀取 AAA.a
println(i.b) // 已經刻意將 b 做了符號讀取連結, 看似直接(實際是間接)讀取
i.b = 3 // 已經刻意將 b 做了符號寫入連結, 看似直接(實際是間接)寫入
println(c.b) // 因為 b 是共享成員, 所以變身的成員也跟著改變
}
結果:
5
10
5
10
3
2019年7月26日 星期五
Kotlin 的 promise
若要用 Kotlin 寫 Javascript (Kotlin to JS), 使用 promise 要先 import kotlin.js.Promise, 接著宣告資料型態, 一般用法像是:
import kotlin.js.Promise
import kotlin.browser.*
fun main() {
lateinit var resolve: (Int)->Unit
val promise = Promise < Int > { Yes, _ ->
resolve = Yes
}
promise.then { result -> // only execute when promise fullfill
println("I got $result")
}
window.setTimeout( {resolve(0)}, 2000) // resolve result sometime or somewhere
}
例如應用在按鍵事件驅動的不定時的異步化(async function)程式上相當實用.
promise 的原理很簡單, 只要沒呼叫 resolve( ) 去解析, 就會維持在暫停(suspend)狀態, 一旦執行過 resolve( ) 後就變成滿足(fullfill)狀態. 後續當呼叫 then { } 區塊時, 若還是在暫停狀態下,沒意外(exception)的話,就維持該狀態,但要將該區塊(指標變數)附加(累積)到 promise 物件裏面,一旦呼叫了 resolve(result), resolve 除了會解析結果外, 同時還會就將結果傳遞給之前所累積的 then { } 區塊去執行. 也就是說只有在滿足狀態當下, promise 的 then { } 區塊才會執行. 有個應用情境是:將 promise.then { } 區塊放在函式內執行, 再用像是 window.setTimeout 或是 window.window.requestAnimationFrame 定時啟動該函式, 重複啟動函式時, 若 promise 一直維持在暫停的狀態, 就會造成 then 區塊不停的累積, 一旦狀態滿足時, 就有可能造成區塊重複執行的後遺症, 例如:
import kotlin.js.Promise
import kotlin.browser.*
fun main() {
lateinit var resolve: (Int)->Unit
val promise = Promise < Int > { Yes, _ ->
resolve = Yes
}
var count = 0
fun render( ) {
val capture = count
promise.then { result -> // dupcate then if not fullfill
println("fullfill: result=${result} capture=${capture}")
}
if( count ++ < 10 ) window.requestAnimationFrame( {render() } )
}
window.requestAnimationFrame({render() })
// promise not resolve
window.setTimeout( {resolve(0)}, 2000) // resolve result sometime or somewhere
}
執行結果:
fullfill: result=0 capture=0
fullfill: result=0 capture=1
fullfill: result=0 capture=2
fullfill: result=0 capture=3
fullfill: result=0 capture=4
fullfill: result=0 capture=5
fullfill: result=0 capture=6
fullfill: result=0 capture=7
fullfill: result=0 capture=8
fullfill: result=0 capture=9
fullfill: result=0 capture=10
為了用 kotlin 實現 async/await 的邏輯. 運用 promise 物件, 基本上就可以成為 async function, await 只能放在 async function 裏面, 直到 promise 完成解析(滿足狀態)才能進行後續動作, 因此特別用高階函式及 skip re-entry 技巧, 將 promise 包進新的類型, 並把他的 then 區塊打包放進高階函式當成 callback 函式去執行, 就能解決重複執行的困擾:
import kotlin.js.Promise
import kotlin.browser.*
class myAsync {
var once = 0
lateinit var resolve:(Int)-> Unit
val promise = Promise < Int > { Yes, _ ->
resolve = Yes
}
fun bwait(callback: (Int) -> Unit ) { // high order function with callback function
promise.then { result ->
if (once ++ == 0) callback(result ) // 只執行第一次的 then 區塊
}
}
fun await(callback: (Int) -> Unit ) { // high order function with callback function
once ++
promise.then { result ->
if (-- once == 0) callback(result ) // 只執行最後一次的 then 區塊
}
}
}
fun main() {
val async = myAsync( )
var count = 0
fun render( ) {
val capture = count
async.await { result -> // no duplicate then if fullfill
println("fullfill: result=${result} capture=${capture}")
}
if( count ++ < 10 ) window.requestAnimationFrame( {render() } )
}
window.requestAnimationFrame( { render() } )
window.setTimeout( {async.resolve(0)}, 2000) // resolve result sometime or somewhere
}
若是用 async.await 等待執行的結果:
fullfill: result=0 capture=10
若是用 async.bwait 等待執行的結果:
fullfill: result=0 capture=0
import kotlin.js.Promise
import kotlin.browser.*
fun main() {
lateinit var resolve: (Int)->Unit
val promise = Promise
resolve = Yes
}
promise.then { result -> // only execute when promise fullfill
println("I got $result")
}
window.setTimeout( {resolve(0)}, 2000) // resolve result sometime or somewhere
}
例如應用在按鍵事件驅動的不定時的異步化(async function)程式上相當實用.
promise 的原理很簡單, 只要沒呼叫 resolve( ) 去解析, 就會維持在暫停(suspend)狀態, 一旦執行過 resolve( ) 後就變成滿足(fullfill)狀態. 後續當呼叫 then { } 區塊時, 若還是在暫停狀態下,沒意外(exception)的話,就維持該狀態,但要將該區塊(指標變數)附加(累積)到 promise 物件裏面,一旦呼叫了 resolve(result), resolve 除了會解析結果外, 同時還會就將結果傳遞給之前所累積的 then { } 區塊去執行. 也就是說只有在滿足狀態當下, promise 的 then { } 區塊才會執行. 有個應用情境是:將 promise.then { } 區塊放在函式內執行, 再用像是 window.setTimeout 或是 window.window.requestAnimationFrame 定時啟動該函式, 重複啟動函式時, 若 promise 一直維持在暫停的狀態, 就會造成 then 區塊不停的累積, 一旦狀態滿足時, 就有可能造成區塊重複執行的後遺症, 例如:
import kotlin.js.Promise
import kotlin.browser.*
fun main() {
lateinit var resolve: (Int)->Unit
val promise = Promise < Int > { Yes, _ ->
resolve = Yes
}
var count = 0
fun render( ) {
val capture = count
promise.then { result -> // dupcate then if not fullfill
println("fullfill: result=${result} capture=${capture}")
}
if( count ++ < 10 ) window.requestAnimationFrame( {render() } )
}
window.requestAnimationFrame({render() })
// promise not resolve
window.setTimeout( {resolve(0)}, 2000) // resolve result sometime or somewhere
}
執行結果:
fullfill: result=0 capture=0
fullfill: result=0 capture=1
fullfill: result=0 capture=2
fullfill: result=0 capture=3
fullfill: result=0 capture=4
fullfill: result=0 capture=5
fullfill: result=0 capture=6
fullfill: result=0 capture=7
fullfill: result=0 capture=8
fullfill: result=0 capture=9
fullfill: result=0 capture=10
為了用 kotlin 實現 async/await 的邏輯. 運用 promise 物件, 基本上就可以成為 async function, await 只能放在 async function 裏面, 直到 promise 完成解析(滿足狀態)才能進行後續動作, 因此特別用高階函式及 skip re-entry 技巧, 將 promise 包進新的類型, 並把他的 then 區塊打包放進高階函式當成 callback 函式去執行, 就能解決重複執行的困擾:
import kotlin.js.Promise
import kotlin.browser.*
class myAsync {
var once = 0
lateinit var resolve:(Int)-> Unit
val promise = Promise < Int > { Yes, _ ->
resolve = Yes
}
fun bwait(callback: (Int) -> Unit ) { // high order function with callback function
promise.then { result ->
if (once ++ == 0) callback(result ) // 只執行第一次的 then 區塊
}
}
fun await(callback: (Int) -> Unit ) { // high order function with callback function
once ++
promise.then { result ->
if (-- once == 0) callback(result ) // 只執行最後一次的 then 區塊
}
}
}
fun main() {
val async = myAsync( )
var count = 0
fun render( ) {
val capture = count
async.await { result -> // no duplicate then if fullfill
println("fullfill: result=${result} capture=${capture}")
}
if( count ++ < 10 ) window.requestAnimationFrame( {render() } )
}
window.requestAnimationFrame( { render() } )
window.setTimeout( {async.resolve(0)}, 2000) // resolve result sometime or somewhere
}
若是用 async.await 等待執行的結果:
fullfill: result=0 capture=10
若是用 async.bwait 等待執行的結果:
fullfill: result=0 capture=0
2019年7月14日 星期日
for Kotlin
一直以來寫 c 或 Javascript 時習慣用:
for (i = 0; i < 10; i++) // ...
在 Kotlin 的 for 迴圈語法只能用 for(variable in Range) 來實現,像上述例子就可用單端式範圍遞增數列運算子 until 來實現:
for (i in 0 until 10) // until 是單端式範圍, 包含 0, 但不含 10
或是改成雙端式範圍遞增數列運算子 .. 加以實現:
for(i in 0 .. 9 ) // .. 是雙端式範圍, 0 與 9 皆包含在內
若累進數(等差級數步階值)大於 1, 就要加上 step, 像上式用 c 或 Javascipt寫的步階改為 2:
for (i = 0; i < 10; i+=2) // ...
在 Kotlin 就直接加上 step 2:
for (i in 0 until 10 step 2) // ...
如果是遞減數列(數字大往數字小的等差級數), 像是 c 或 Javascript 所寫雙端式範圍的迴圈:
for (i=10; i>=0; i-=2) // 10, 8, 6, ..., 0
因為在 Kotlin 語法中, 步階值必須是正整數, 雙端式範圍遞減數列用 downTo 關鍵字來實現:
for (i in 10 downTo 0 step 2) // ...
參考文章: https://kotlinlang.org/docs/tutorials/kotlin-for-py/loops.html
for (i = 0; i < 10; i++) // ...
在 Kotlin 的 for 迴圈語法只能用 for(variable in Range) 來實現,像上述例子就可用單端式範圍遞增數列運算子 until 來實現:
for (i in 0 until 10) // until 是單端式範圍, 包含 0, 但不含 10
或是改成雙端式範圍遞增數列運算子 .. 加以實現:
for(i in 0 .. 9 ) // .. 是雙端式範圍, 0 與 9 皆包含在內
若累進數(等差級數步階值)大於 1, 就要加上 step, 像上式用 c 或 Javascipt寫的步階改為 2:
for (i = 0; i < 10; i+=2) // ...
在 Kotlin 就直接加上 step 2:
for (i in 0 until 10 step 2) // ...
如果是遞減數列(數字大往數字小的等差級數), 像是 c 或 Javascript 所寫雙端式範圍的迴圈:
for (i=10; i>=0; i-=2) // 10, 8, 6, ..., 0
因為在 Kotlin 語法中, 步階值必須是正整數, 雙端式範圍遞減數列用 downTo 關鍵字來實現:
for (i in 10 downTo 0 step 2) // ...
參考文章: https://kotlinlang.org/docs/tutorials/kotlin-for-py/loops.html
2019年7月5日 星期五
使用 kotlinc-js 編譯程式碼快速學習 Kotlin 語言
1. 到官網下載 kotlin compiler 編譯器(kotlin-compiler-1.3.40.zip), 用 unzip 解壓縮並放在適當目錄(例如 pathToKotlinc), 編譯的執行檔會在目錄 bin 底下, 程式庫在目錄 lib 底下. 再將 lib 目錄下的檔案 kotlin-stdlib-js.jar 用 unzip 解壓縮至適當位置, 只提取裡面的 kotlin.js 及 kotlin.js.map (debug 時會用到)檔案就可以了, 將他複製到工作目錄, 例如:
cd
mkdir work
cd work
unzip pathToKotlinc/lib/kotlin-stdlib-js.jar
2. 在工作目錄用文字編輯器寫一個 kotlin 程式, 例如:
geany my.kt &
3. 將以下作複數運算的範例程式存成檔案 my.kt
import kotlin.math.*
data class C(var r:Double, var i:Double) { // C: Complex number, r: real part, i:image part
val square:Double = r*r + i*i // |C|2
val length:Double = sqrt(square) // abs(C) = |C| = sqrt(|C|2)
operator fun plus(a:C):C { // +
r += a.r
i += a.i
return this
}
operator fun minus(a:C):C { // -
r -= a.r
i -= a.i
return this
}
operator fun times(a:C):C { // *
val temp = a.r * r - a.i * i // real part
i = a.i*r + a.r*i // image part
r = temp
return this
}
operator fun div(a:C):C { // ÷
if (a.length > 0f) {
val temp = (a.r*r + a.i * i) / a.square // real part
i = (a.i*r - a.r*i) / a.square // image part
r = temp
} else throw Exception("Divide by zero(length=0)")
return this
}
operator fun invoke( ):C{ // dump self complex number
val real = floor(r*100+0.5).toFloat()/100 // 實部只顯示到小數點 2 位, 4 捨 5 入
val imag = floor(i*100+0.5).toFloat()/100 // 虛部只顯示到小數點 2 位, 4捨 5 入
if (imag < 0) println("$real - ${-imag} i") else println("$real + $imag i")
return this
}
}
fun main(){
try {
val c = C(3.0, 3.0) / C(5.0, 2.0) // c = (3 + 3i) / (5 + 2i)
c( ) // dump
} catch (e:Exception){
println(e.message)
}
}
4. 用 kotlin 的編譯器 kotlinc-js 將上述 kotlin 原始碼翻譯為 Javascript:
pathToKotlinc/bin/kotlinc-js my.kt -output my.js
或是利用 geany 的設定組建命令, 直接點選工具欄: 組建(B) - > 設定組件命令(S), 第一欄隨意輸入識別名稱(例如 kotlin2js), 第二欄輸入 kotlin2js 編譯器的位置及參數(%f 是目前編輯檔案名稱, %e 是不含副檔名的檔案名稱, -output 可以指定輸出的檔案名稱), 例如:
pathToKotlinc/bin/kotlinc-js %f -output %e.js
意思是說用 kotlinc-js 將目前的檔案翻譯成 Javascript, 輸出檔與編譯的同名稱但副檔名改為 js, 第三欄可以空白預設是目前的工作目錄, 設定好後, 用滑鼠點選: 組建 -> kotlin2js 執行看看, 應該會在工作目錄編譯出 my.js.
若要在 vscode 裏面編譯, 參考 https://code.visualstudio.com/docs/editor/variables-reference, 編寫一個 tasks.json 檔案, 點選 Terminal -> Configure Tasks帶出編輯視窗, 把以下內容寫入:
{
"tasks": [{
"label": "Kotlinc2js",
"type": "shell",
"command": "pathToKotlinc/bin/kotlinc-js",
"args": ["${file}", "-output", "${fileBasenameNoExtension}.js"],
"group": {
"kind": "build",
"isDefault": true
}
}]
}
存檔後, 點選 Terminal -> Run Build task 或是用快速按鍵 Ctrl-Shift-B, 就能將目前編輯的 Kotlin 程式檔轉換成 Javascrip
5. 在工作目錄底下用文字編輯器另外編輯一個 my.html 準備讓瀏覽器開啟, 並在開發工具底下執行用 kotlin 語言所寫的程式碼(實際上是翻譯成兩個 Javascript 檔案: kotlin.js 及 my.js). 將以下內容存檔(my.html):
<html><head><meta charset="UTF-8"></head>
<body>
<script type="text/javascript" src="kotlin.js"></script>
<script type="text/javascript" src="my.js"></script>
</body>
</html>
6. 用瀏覽器 chrome 或是 firefox 打開上述 my.html 檔案, 接著按滑鼠右鍵點選檢查原始碼來開啟內建的開發工具, 在開發工具欄點選 console 就能看到程式的輸出結果
cd
mkdir work
cd work
unzip pathToKotlinc/lib/kotlin-stdlib-js.jar
2. 在工作目錄用文字編輯器寫一個 kotlin 程式, 例如:
geany my.kt &
3. 將以下作複數運算的範例程式存成檔案 my.kt
import kotlin.math.*
data class C(var r:Double, var i:Double) { // C: Complex number, r: real part, i:image part
val square:Double = r*r + i*i // |C|2
val length:Double = sqrt(square) // abs(C) = |C| = sqrt(|C|2)
operator fun plus(a:C):C { // +
r += a.r
i += a.i
return this
}
operator fun minus(a:C):C { // -
r -= a.r
i -= a.i
return this
}
operator fun times(a:C):C { // *
val temp = a.r * r - a.i * i // real part
i = a.i*r + a.r*i // image part
r = temp
return this
}
operator fun div(a:C):C { // ÷
if (a.length > 0f) {
val temp = (a.r*r + a.i * i) / a.square // real part
i = (a.i*r - a.r*i) / a.square // image part
r = temp
} else throw Exception("Divide by zero(length=0)")
return this
}
operator fun invoke( ):C{ // dump self complex number
val real = floor(r*100+0.5).toFloat()/100 // 實部只顯示到小數點 2 位, 4 捨 5 入
val imag = floor(i*100+0.5).toFloat()/100 // 虛部只顯示到小數點 2 位, 4捨 5 入
if (imag < 0) println("$real - ${-imag} i") else println("$real + $imag i")
return this
}
}
fun main(){
try {
val c = C(3.0, 3.0) / C(5.0, 2.0) // c = (3 + 3i) / (5 + 2i)
c( ) // dump
} catch (e:Exception){
println(e.message)
}
}
4. 用 kotlin 的編譯器 kotlinc-js 將上述 kotlin 原始碼翻譯為 Javascript:
pathToKotlinc/bin/kotlinc-js my.kt -output my.js
或是利用 geany 的設定組建命令, 直接點選工具欄: 組建(B) - > 設定組件命令(S), 第一欄隨意輸入識別名稱(例如 kotlin2js), 第二欄輸入 kotlin2js 編譯器的位置及參數(%f 是目前編輯檔案名稱, %e 是不含副檔名的檔案名稱, -output 可以指定輸出的檔案名稱), 例如:
pathToKotlinc/bin/kotlinc-js %f -output %e.js
意思是說用 kotlinc-js 將目前的檔案翻譯成 Javascript, 輸出檔與編譯的同名稱但副檔名改為 js, 第三欄可以空白預設是目前的工作目錄, 設定好後, 用滑鼠點選: 組建 -> kotlin2js 執行看看, 應該會在工作目錄編譯出 my.js.
若要在 vscode 裏面編譯, 參考 https://code.visualstudio.com/docs/editor/variables-reference, 編寫一個 tasks.json 檔案, 點選 Terminal -> Configure Tasks帶出編輯視窗, 把以下內容寫入:
{
"tasks": [{
"label": "Kotlinc2js",
"type": "shell",
"command": "pathToKotlinc/bin/kotlinc-js",
"args": ["${file}", "-output", "${fileBasenameNoExtension}.js"],
"group": {
"kind": "build",
"isDefault": true
}
}]
}
存檔後, 點選 Terminal -> Run Build task 或是用快速按鍵 Ctrl-Shift-B, 就能將目前編輯的 Kotlin 程式檔轉換成 Javascrip
5. 在工作目錄底下用文字編輯器另外編輯一個 my.html 準備讓瀏覽器開啟, 並在開發工具底下執行用 kotlin 語言所寫的程式碼(實際上是翻譯成兩個 Javascript 檔案: kotlin.js 及 my.js). 將以下內容存檔(my.html):
<html><head><meta charset="UTF-8"></head>
<body>
<script type="text/javascript" src="kotlin.js"></script>
<script type="text/javascript" src="my.js"></script>
</body>
</html>
Kotlin 的 Delegated Properties (被代理的變數或特性)
參考文章: https://kotlinlang.org/docs/reference/delegated-properties.html
針對類型裡的特性(propertys), 常數(val)的特性內定 getter 是用 get( ) 方法去讀取主角(field)的值, 對於可寫入的特性(var)而言, 內定 setter 則用 set(v: Type) 方法設定幕後的主角(field), 除非要另外處理 field 的運算才需改寫, 否則 kotlin 會自動加上必要程式碼:
class SomeClass {
var v: String = "?"
// get( ) { return field }
// set(s:String) { field = s }
val readonly: Int = 0
// get( ) { return field }
}
物件在引用特性時不必後綴成對小括號, 因此很簡潔. 物件引用方法( ) 時可以看做是一個運算的結果, 也就是所謂的函數. Kotlin 針對特性或變數有個特別條款可以用 by 跑龍套, 也就是說留待跑龍套代為處置, Koltin 已經為跑龍套主角(object)指定了兩個運算 operator getValue( ) 及 operator setValue( ) 來處理特性或變數的讀寫機制, 若想改寫,可以 import pacage 針對讀與寫操作分別作運算,其中對應 get( ) 方法就是 operator getValue( ), 對應 set( ) 方法就是 operator setValue( ), 詳如範例:
import kotlin.reflect.*
class InitString (str: String) { // 代理的主角類型
var bakendField: String = str // 真正被讀寫的幕後主角
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
print(property.name + " : ")
return bakendField // return value for method get( ), 讀取
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, v: String) {
println(property.name + " = " + v)
bakendField = v // set value to property for method set(v), 寫入
}
}
fun main(){
var c : String by InitString("???") // 用物件 InitString( ) 代理改寫變數的讀/寫機制
println(c) // 讀取變數 c
c = "dd" // 設定變數 c
println(c) // 重新讀取變數 c
}
執行結果:
c : ???
c = dd
c : dd
針對類型裡的特性(propertys), 常數(val)的特性內定 getter 是用 get( ) 方法去讀取主角(field)的值, 對於可寫入的特性(var)而言, 內定 setter 則用 set(v: Type) 方法設定幕後的主角(field), 除非要另外處理 field 的運算才需改寫, 否則 kotlin 會自動加上必要程式碼:
class SomeClass {
var v: String = "?"
// get( ) { return field }
// set(s:String) { field = s }
val readonly: Int = 0
// get( ) { return field }
}
物件在引用特性時不必後綴成對小括號, 因此很簡潔. 物件引用方法( ) 時可以看做是一個運算的結果, 也就是所謂的函數. Kotlin 針對特性或變數有個特別條款可以用 by 跑龍套, 也就是說留待跑龍套代為處置, Koltin 已經為跑龍套主角(object)指定了兩個運算 operator getValue( ) 及 operator setValue( ) 來處理特性或變數的讀寫機制, 若想改寫,可以 import pacage 針對讀與寫操作分別作運算,其中對應 get( ) 方法就是 operator getValue( ), 對應 set( ) 方法就是 operator setValue( ), 詳如範例:
import kotlin.reflect.*
class InitString (str: String) { // 代理的主角類型
var bakendField: String = str // 真正被讀寫的幕後主角
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
print(property.name + " : ")
return bakendField // return value for method get( ), 讀取
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, v: String) {
println(property.name + " = " + v)
bakendField = v // set value to property for method set(v), 寫入
}
}
fun main(){
var c : String by InitString("???") // 用物件 InitString( ) 代理改寫變數的讀/寫機制
println(c) // 讀取變數 c
c = "dd" // 設定變數 c
println(c) // 重新讀取變數 c
}
執行結果:
c : ???
c = dd
c : dd
訂閱:
文章 (Atom)