0%

過去兩年的時間投入了數據平台的建置,希望可以打造一個所謂的現代化數據平台(Modern Data Platform),這中間可是比想像中困難多了。但兩年過去了團隊成員從也是從小貓幾隻,一路成長到十多人的團隊。講到現代化的數據平台,NoSQL的儲存方案肯定是會被考量到的一項技術,在一陣研究和討論後,我們決定採用Cassandra當作在數據平台服務中,擔任NoSQL的主要儲存技術。

在導入的過程中當然沒有這麼順利,特別是對於只用過Oracle或是SQL Server等RDBMS技術,完全沒有碰過NoSQL的開發團隊,大家對於使用Cassandra總是有錯誤的想像,或是在不熟悉與不合適的使用情境下,最後造成大家與Cassandra「不歡而散」。

這篇想開始談談實際使用Cassandra後會因為Cassandra的設計限制遇到的問題,希望大家在一頭熱投入Cassandra的研究前,可以評估使用情境是否適合。我特別推薦在準備DataStax的Cassandra Developer Certification時上到的一堂課,這堂DS220提到許多Cassandra在實在作data modeling會需要知道的技巧和特性。

回到這次想談的,CQL不是SQL。

在初期投入Cassandra技術評估,看到Cassandra支援CQL(Cassandra Query Language)時,肯定會讓RDBMS開發者眼睛一亮,這看起來多麼的熟悉,忍不住在比較NoSQL方案時在Cassandra蓋上一個優勝的章。當然我不能否認當初我們也這麼認為,一直到大家實際使用時,才會發現CQL和SQL有根本上的不同。

不是任意欄位都能FILTER

簡單來說,比起RDBMS的SQL查詢,Cassandra因為本身的設計,所以CQL本來就只能作到有限度的查詢。CQL不能像SQL一樣可以將任意欄位放在where條件執行查詢,而是只能針對設定成partition key和clustering key的欄位進行查詢,甚至對組成clustering key的欄位,還有查詢順序的限制。當然這裡我們就先不提建立secondary index或是materialize view的情況。

拿下面這個簡單的員工資料表來說明。

deptname jobgrade gender emplid name
HR 1 Male 001 Tom
HR 3 Female 002 Amy
Design 2 Male 003 John
Design 1 Female 004 Alice
HR 1 Male 005 Bob
Design 3 Male 006 Henry

在Cassandra的員工表作了以下設計,如果不說是Cassandra的話,看起來其實就和一般的RDBMS沒有差別。但在Cassandra中下面建立的員工表其實隱含了將deptName設定為partition key,clustering key設定為jobgrade、gender和emplid的意思。以下的語法可以把這個範例建起來,DataStax有提供線上的serverless database可以使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-- create
CREATE TABLE CASSANDRA.EMPLOYEE (
deptName text,
jobgrade text,
empId text,
name text,
gender text,
primary key(deptName, jobgrade, gender, empId)
);

-- insert
INSERT INTO CASSANDRA.EMPLOYEE(deptName,jobgrade,empId,name,gender) VALUES ('HR', '1', '001', 'Tom', 'Male');
INSERT INTO CASSANDRA.EMPLOYEE(deptName,jobgrade,empId,name,gender) VALUES ('HR', '3', '002', 'Amy', 'Female');
INSERT INTO CASSANDRA.EMPLOYEE(deptName,jobgrade,empId,name,gender) VALUES ('Design', '2', '003', 'John', 'Male');
INSERT INTO CASSANDRA.EMPLOYEE(deptName,jobgrade,empId,name,gender) VALUES ('Design', '1', '004', 'Alice', 'Female');
INSERT INTO CASSANDRA.EMPLOYEE(deptName,jobgrade,empId,name,gender) VALUES ('HR', '1', '005', 'Mary', 'Female');
INSERT INTO CASSANDRA.EMPLOYEE(deptName,jobgrade,empId,name,gender) VALUES ('Design', '3', '006', 'Henry', 'Male');

-- select
SELECT * FROM CASSANDRA.EMPLOYEE;

deptname | jobgrade | gender | empid | name
----------+----------+--------+-------+-------
HR | 1 | Female | 005 | Mary
HR | 1 | Male | 001 | Tom
HR | 3 | Female | 002 | Amy
Design | 1 | Female | 004 | Alice
Design | 2 | Male | 003 | John
Design | 3 | Male | 006 | Henry

假設在RDBMS裡用以上建立的資料表,舉幾種查詢情境如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
-- 查詢jobgrade為1的員工資料
SELECT *
FROM EMPLOYEE
WHERE jobgrade = '1'

deptname | jobgrade | gender | empid | name
----------+----------+--------+-------+-------
HR | 1 | Male | 001 | Tom
HR | 1 | Female | 005 | Mary
Design | 1 | Female | 004 | Alice

-- 查詢deptname為HR, gender為Male的名字
SELECT name
FROM EMPLOYEE
WHERE deptname = 'HR'
AND gender = 'Male'

name
------
Tom

--查詢deptname為HR, jobgrade為1的員工資料
SELECT *
FROM EMPLOYEE
WHERE deptname = 'HR'
AND jobgrade = '1'

deptname | jobgrade | gender | empid | name
----------+----------+--------+-------+------
HR | 1 | Female | 005 | Mary
HR | 1 | Male | 001 | Tom

以上看起來很直覺的查詢,如果是在Cassandra的世界裡,執行第一支CQL會得到以下的錯誤:

1
2
3
4
5
SELECT * 
FROM CASSANDRA.EMPLOYEE
WHERE jobgrade = '1'

InvalidRequest: Error from server: code=2200 [Invalid query] message="Cannot execute this query as it might involve data filtering and thus may have unpredictable performance. If you want to execute this query despite the performance unpredictability, use ALLOW FILTERING"

Cassandra會將資料依partitiony key分散放到不同的partition存放,所以在CQL查詢中的第一個條件必須要帶partition key(也就是deptname),Cassandra才能直接到指定的partition取回資料,用來加速資料查詢的速度。因此在不給定partition key條件下直接用jobgrade作filter是不允許的。

第二支CQL執行則會得到以下的錯誤:

1
2
3
4
5
6
SELECT name 
FROM CASSANDRA.EMPLOYEE
WHERE deptname = 'HR'
AND gender = 'Male'

InvalidRequest: Error from server: code=2200 [Invalid query] message="PRIMARY KEY column "gender" cannot be restricted as preceding column "jobgrade" is not restricted"

因為Cassandra的世界裡,會將一個partition內部的資料依據clustering key作排序。以員工資料表為例,會以deptname切partition後,再依序用jobgrade、gender和emplid排序。

Casssandra透過預先排序的方式,在查詢資料時就可以得到排序結果,和SQL查詢完才作order by不同。因此Cassandra嚴格限制clustering key的欄位必須要依順序作filter,所以跳過jobgrade先查詢gender是不允許的。

第三支CQL則因為符合partition和clustering限制所以可以成功執行。

1
2
3
4
5
6
7
8
9
 SELECT *
FROM CASSANDRA.EMPLOYEE
WHERE deptname = 'HR'
AND jobgrade = '1'

deptname | jobgrade | gender | empid | name
----------+----------+--------+-------+------
HR | 1 | Female | 005 | Mary
HR | 1 | Male | 001 | Tom

由以上可以發現,CQL雖然和SQL語法非常相似,但本質上Cassandra就是NoSQL資料庫,也有本身的設計限制。因此對於SQL使用者來說,千萬不要因為看到很像SQL的語法,就覺得可以很容易的上手使用!

上一篇有提到真的有重要的功能不支援web的情況下,可以採用conditional import的方式,在不同平台運行不同的程式片段,避免使用過多的平台判斷邏輯造成程式碼過長,或是在compile階段出現error。

假設今天要實作一個platform判斷邏輯,因為我們已經知道web平台不支援dart:io,且其他的平台(ios, android等)都可以用dart:io.Platform。所以設計的邏輯為,當dart:io能被使用時採用dart:io.Platform判斷運行平台,否則即判定為web。

首先可以先作一個簡單的列舉項目PlatformType,用來比較platform判斷結果。接著設計一個PlatformCheck類別,並實作類似dart:io的isPlatform檢查。而checkPlatform函式會實作在拆出去的platform_web或platform_io兩個dart裡面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// platform_type.dart
import 'platform_web.dart' if (dart.library.io) 'platform_io.dart';

enum PlatformType {
WEB,
ANDROID,
IOS
}
class PlatformCheck {
static bool isWeb() => checkPlatform() == PlatformType.WEB;
static bool isAndroid() => checkPlatform() == PlatformType.ANDROID;
static bool isIos() => checkPlatform() == PlatformType.IOS;
// ...other platform
}

回頭看開頭的conditional import,我們想到達成的效果為,如果可以使用dart.library.io的話就import platform_io.dart,不行的話則import platform_web.dart。

所以只要分別在platform_io.dart實作非web平台的checkPlatform函式,接著在platform_web.dart實作web平台的checkPlatform函式即可。
如果今天app是運行在可以使用dart:io的非web平台上,platform_io.dart就會被import進來,接著就可以在checkPlatform裡面實作使用dart:io.Platform的平台判斷邏輯。

1
2
3
4
5
6
7
8
9
10
// platform_io.dart
import 'dart:io' show Platform;
import 'package:webapp/platform_type.dart';

PlatformType checkPlatform() {
if (Platform.isIOS) return PlatformType.IOS;
if (Platform.isAndroid) return PlatformType.ANDROID;

return PlatformType.IOS;
}

如果app是運行在不能使用dart:io的web平台上,會變成import platform_web.dart,因為目前只有web平台無法使用dart:io,所以如果是web情況下checkPlatform就不作其他的邏輯判斷,直接回傳是web。

到這裡可以再往回看一次,如果是運行在web平台上,在程式compile時就完全不會import到platform_io.dart,可以避免在web平台上import dart:io造成的compile錯誤。

1
2
3
4
5
6
// platform_web.dart
import 'package:webapp/platform_type.dart';

PlatformType checkPlatform() {
return PlatformType.WEB;
}

最後只要import自己寫的platform_type.dart,就可以用以下的語法達成跨平台的platform檢查。如果今天app是運行在web之下,就會回傳web。

1
2
3
4
5
6
String currPlatform = "";
if (PlatformCheck.isWeb()) currPlatform = 'Web';
else if (PlatformCheck.isAndroid()) currPlatform = 'Android';
else if (PlatformCheck.isIos()) currPlatform = 'iOS';

>>> WEB

如果專案在考量到web平台下,勢必會遇一個功能在不同的平台上要運行不同的package或是程式片段的跨平台問題。比起使用多個if-else的platform判斷邏輯,再把所有程式碼都寫進每個if-else中,使用conditaional import的方式可以有效的規劃程式碼,也可以避免import到平台不支援的package造成的compile錯誤。

參考資料:
Conditional imports across Flutter and Web

打開Flutter web開發的方法並不困難,但在將手機app轉換(migration)到web app時會遇到許多轉換問題。在Flutter Engage的發佈會上官方有特別展示web migration要注意的地方。而使用Flutter開發跨平台應用程式時,第一個要考量到的就是package的跨平台兼容性。

使用pub.dev確認package兼容性

為了確保使用到的package在所有的平台都能正常的執行,專案在評估採用package時就必須確認是否兼容所有要發佈的平台,或是未來可能會發佈的平台。

剛起頭的專案在pub.dev尋找可採用的package時,就可以邊從資訊頁上方找到平台支援資訊。如果是從mobile轉移到web,就建議要掃過所有使用到的package,當然也可以直接把web app在debug模式跑起來就知道哪些不能用了。

dart:io不支援web的解決方法

dart:io可以協助處理File、Socket、HTTP與其他I/O任務,官方的Web FAQdart:io api都有特別寫到dart:io是不支援web app的。自己的經驗有遇過以下兩種情況有使用到dart:io須要特別注意:

1. 使用http處理http request

有些專案會使用dart:io的HttpClient處理跟Http server之間的request與response。但因為dart:io不支援web平台,所以官方建議使用http進行跨平台的http處理,在開發時也可以參考官方的Networking Cookbook有詳細的介紹。

2. 使用universal_platform檢查運行平台

一般跨平台專案會使用dart:io.Platform判斷當前的運行平台,但dart:io.Platform並不支援web,所以在運行於web環境時會出現Unsupported operation: Platfor._operationSystem錯誤。

1
2
3
4
5
6
7
8
9
import 'dart:io' show Platform;

if (Platform.isAndroid) {
// Do something on Android
} else if (Platform.isIOS) {
// Do something on iOS
} else {
// I want do something on web, but I only get Unsupported operation: Platform._operatingSystem
}

考量到web的情境下,另一個方法可以使用kIsWeb再搭配Theme.of與TargetPlatform就可以達到避開使用dart:io.Platform又能夠在建立widget時判斷當下的運行平台。

1
2
3
4
5
6
7
8
9
10
import 'package:flutter/foundation.dart' show kIsWeb, TargetPlatform;

final platform = Theme.of(context).platform;
if (kIsWeb) {
// Do something on Web
} else if (platform == TargetPlatform.android) {
// Do something on Android
} else if (platform == TargetPlatform.iOS) {
// Do something on iOS
}

但比較適合的方法為採用conditional import來解決。當dart:io能被import時採用dart:io.Platform判斷運行平台,無法被import的情況下判定為web。這種方式因為已經有被實作過了(universal_platform),所以可以考慮不用再自己實作。

使用conditional import在不同平台上運行不同程式片段

如果真的有一個重要的功能,但又不支援web該怎麼辦?像是上面提到只在mobile使用dart:io判斷運行平台,或是須要在mobile上用dart:io的HttpClient處理proxy問題,然後在web單純使用http處理與server的溝通。

若無法確保package跨平台兼容,相同feature在不同平台上要執行不同程式片段時,較簡單的方法是使用上述提到的平台判斷。例如使用universal_platform判斷當下運行的平台後,再執行於不同平台要執行的函式或是widget。

1
2
3
4
5
6
7
8
9
10
11
import 'package:universal_platform/universal_platform.dart';
import 'dart:js' // web platform need this, but it will cause Not Found Error on Android/iOS


if (UniversalPlatform.isWeb) {
// Do something or render widget on Web
} else if (UniversalPlatform.isAndroid) {
// Do something or render widget on Android
} else if (UniversalPlatform.isIOS) {
// Do something or render widget on iOS
}

但如果功能複雜度高且程式碼又很長的話,程式片段會變很長易讀性也會變差。另外若使用到只有web能用的package,也會造成在其他平台compile階段出現錯誤。這時會建議將不同平台要執行的程式片段拆成不同dart,並使用conditional import在開頭就決定要import哪個dart進來使用。

import “web_support_func.dart” if (dart.library.io) “non_web_support_func.dart”

以上是進行web migration馬上會遇到的跨平台兼容問題,除了專案本身使用到的第三方package可能不支援web平台外,要特別注意因為dart:io不支援web引發的http request還有platform判斷問題。如果真的有feature要在不同平台執行不同程式片段時,建議使用conditional import來設計程式碼。

在使用CanvasKit渲染器開發Flutter web app時,會在app啟動時從CDN服務下載CanvasKit,因此在採用CanvasKit渲染器的情況下預設是需要有連網的環境,否則app在啟動時會因為無法下載CanvasKit出現Failed to load resource: net::ERR_INTERNET_DISCONNECTED的錯誤訊息。

但如果是在有連網限制的開發環境下,雖然可以在開發環境使用html渲染器進行開發,並在佈署環境使用CanvasKit渲染器來達到最佳的操作效果,但因為兩種渲染器方法不同,所以渲染出來的效果還是有所差異,這會造成開發和佈署的版面不一致。

雖然預設需要連網才能使用CanvasKit,但目前版本(2.2.2)可以透過指定CanvasKit URL的方式置換掉預設的URL,而且也支援將CanvasKit與web檔案bundle在一起佈署,達到在離線或是有連網限制的環境下執行。但這個方法目前只支援profile與release模式,不支援debug模式

下載CanvasKit放入web資料夾

下載canvaskit.js與canvaskit.wasm並放進web資料夾,舉例來說目前版本使用的CanvasKit為0.25.1,可以先從CDN下載canvaskit.jscanvaskit.wasm。如果要使用profile模式就將兩個檔案在進/web/profiling/目錄內,要使用release模式的話直接放在/web/目錄。

修改url指到CanvasKit位置

透過--dart-define覆寫FLUTTER_WEB_CANVASKIT_URL環境變數,並指到下載的CanvasKit檔案位置。因為這裡的預設根目錄會是web/,所以如果用上述的檔案放法的話只要給/就行了。這樣在profile模式下會自動在/web/profiling/讀取CanvasKit,在release模式會自動在根目錄(預設輸出的local位置為/web/,實際佈署會存在於根目錄)讀取CanvasKit。

使用profile模式:

flutter run -d chrome --profile --dart-define=FLUTTER_WEB_CANVASKIT_URL=/

使用release模式:

flutter build web --dart-define=FLUTTER_WEB_CANVASKIT_URL=/

使用profile模式也可以透過在VS Code設定launch.json檔中的flutterMode與args參數,接著直接在VS Code執行啟動偵錯就可以進入profile模式。如果是使用release模式也可以透過dhttpd在本地端啟動web server測試。

1
2
3
4
5
6
7
8
9
10
11
// launch.json
"configurations": [
{
"name": "Flutter",
"type": "dart",
"request": "launch",
"program": "lib/main.dart",
"flutterMode": "profile",
"args": ["--dart-define=FLUTTER_WEB_CANVASKIT_URL=/"]
}
]

在專案中引用Roboto預設字型

如果app內Text相關widget沒有指定字型(fontFamily),或是雖然有指定字型但沒有將字型檔(.ttf/.otf)放進專案並在pubspec宣告,使用CanvasKit渲染器時會預設從fonts.google.com下載Roboto-Regular使用。

因此要先將Roboto放在專案並宣告在pubspec,在web啟動時就會自動使用專案內的字型檔而不會連到google下載字型。如果有使用到其它字型,可以參考官方的字型引用方法。

從google下載下來會有Rotobo全部的ttf檔,只要把Roboto-Regular放進來即可

接著在pubspec宣告引用

1
2
3
4
5
6
flutter:
uses-material-design: true
fonts:
- family: Roboto
fonts:
- asset: assets/fonts/Roboto-Regular.ttf

預設app啟動就會自動使用Roboto而不會連到google下載字型。

在Flutter官方提供更簡易的支援前,可以使用上述的三個步驟來達成在連網限制的環境上使用CanvasKit渲染器,在部署時只要將CanvasKit檔案一起部署即可。

參考資料:
Flutter issues - Can’t build web apps with CanvasKit without internet
Flutter issues - support bundling CanvasKit instead of CDN
Flutter issues - support specifying CanvasKit URL in debug builds

在Flutter正式將web支援放進stable channel後,現在只要從官網下載最新的stable版sdk,就可以直接開發與佈建web應用程式。這篇會紀錄Flutter web開發從前置準備到發佈前的環節,提供給有Flutter經驗而且要嘗試打開web支援的開發者。如果需要從頭開始建立Flutter的開發環境,可以依不同開發平台參考flutter.dev的說明。

前置準備

Windows開發為例,在完成Flutter開發環境與IDE設定後,就可以執行flutter doctor來協助診斷是否設定完全。其中可以看到開發web需要使用chrome所以會有一項檢查chrome是否有安裝。

flutter doctor

1
2
3
4
5
6
7
8
9
Running "flutter pub get" in flutter_tools...                       8.4s
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 2.2.2, on Microsoft Windows [Version 10.0.19041.1052], locale zh-TW)
[√] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
[√] Chrome - develop for the web
[√] Android Studio (version 4.0)
[√] VS Code (version 1.57.1)
[√] Connected device (2 available)
• No issues found!

其實Flutter也可以使用Edge,所以使用flutter devices來確定有連接到的device,如果有安裝Edge也會被偵測到。

flutter devices

1
2
3
4
2 connected devices:

Chrome (web) • chrome • web-javascript • Google Chrome 91.0.4472.106
Edge (web) • edge • web-javascript • Microsoft Edge 91.0.864.54

建立Flutter專案與打開web支援

如果是要從頭建立一個專案,可以直接使用flutter create [project name],建立的新專案就會帶有web資料夾,裡面會包含所有web需要的檔案像是index、manifest與icons。如果是已經存在的專案但要打開web支援,則可以使用flutter create [project path]就會自動產生web目錄。

flutter create [project name]

設定IDE(VSCode)

打開VSCode點擊右下角設定,就能夠選擇現在可使用的emulator,為了開發web所以這裡選擇chrome。如果不先選擇emulator在執行run debug時也會跳出來請你選擇。

如果需要在debug的時候帶進參數,可以在launch.json檔裡面寫入args。例如Flutter web預設使用的渲染器是Auto模式,即在桌上裝置使用CanvasKit,在行動裝置使用html。如果要指定渲染器都使用html的話,就可以寫進args。

運行debug模式

再來可以在IDE啟動偵錯就會開始debug模式,除了使用IDE啟動偵錯外也可以透過指令flutter run來運行debug模式。因為Flutter會運行dartdevc並對app進行compile將dart轉成javascript,所以第一次啟動debug模式的時間會比較長。web開發可以使用hot restart,Flutter只會對有更動到的部份recompile成javascript,而不需要從頭compile整個app。在IDE可以透過restart debug功能觸發hot restart,如果是使用command line可以按R觸發。

flutter run -d chrome

1
2
3
4
5
6
7
8
9
Launching lib\main.dart on Chrome in debug mode...
Waiting for connection from debug service on Chrome... 22.7s
This app is linked to the debug service: ws://127.0.0.1:50526/2kZvThD-K8w=/ws
Debug service listening on ws://127.0.0.1:50526/2kZvThD-K8w=/ws

Running with sound null safety

To hot restart changes while running, press "r" or "R".
For a more detailed help message, press "h". To quit, press "q".

建構web發佈版本

在開發完準備要發佈後,可以使用flutter build web指令來建構發佈版本。預設發佈時使用的渲染器是auto模式(即在桌上裝置使用canvaskit,在行動裝置使用html),如果要指定使用特定的渲染器,可以使用--web-renderer。

flutter build web --web-renderer [auto/html/canvaskit]

執行指令後Flutter會使用dart2js將app compile成一個單一的javascript檔案main.dart.js,發佈完後會在build目錄內產生web目錄,這整包資料會包含所有必要的檔案並且需要同時佈署。

使用dhttpd於本地端運行

最後在實際將整包web佈署到server上之前,可以使用dhttpd嘗試在本地端先運行app,檢視要發佈的app的功能與asset是否都正常。dhttpd安裝方法可以參考dhttpd在pub.dev的安裝方法,並在運行時指定build/web/目錄就可以在本地端運行要發佈的app,在本地端運行app檢查沒問題後就可以準備將web佈署到正式的server了。

dart pub global run dhttpd –path build/web/

1
Server started on port 8080

Flutter的web支援目前可以很容易的將自己開發好的app轉成web上線,而且設定到發佈其實也不困難,可以達成快速的支援跨平台的需求。但實際在使用時還是會遇到像是將app轉成web app所會遇到的migration問題,或是渲染器選擇使用跟canvaskit預設不支援offline模式問題,而且發佈完成的app本身會是一個PWA(Progressive Web Apps)需要設定manifest。這些問題會在實際實作的過程會遇到,有些自己踩過的坑也會再持續紀錄下來。

參考資料:
Fluter docs - Web

最近幾個月來工作的職務上有些轉換,加入到一個小規模的app開發團隊,並且嘗試導入Flutter開發一套codebase就能夠佈署到多個平台的跨平台app。其實網路上已經有非常多介紹Flutter的文章,而原生與跨平台的解決方案比較也很多人討論過,所以這篇會以一個初入Flutter的新手,到成功交付產品上線後,一直到採用Flutter Web的心路歷程分享。

老闆問:能不能弄個跨平台app

其實跨平台一直都存在並且持續有新的技術出現,雖然Flutter還很年輕,但是在這幾年的熱度也快速的竄升。我認為採用跨平台技術對於單純開發給公司內部使用的系統來說,最大的優點就是在人力的節省。在一個開發了web就會被問能不能放在行動裝置上,作了手機app就會被問能不能web也有的環境下,為了提供多個平台管道給使用者,一支系統難道不能只開發一次就好了嗎(彷彿老闆的聲音飄出來了)。

要開發多個平台可以使用的應用程式,除了能夠支援不同裝置尺寸的web應用程式外,如果需求面上又希望能保留行動裝置功能的支援,那Flutter自然是一個適合的選擇。

Flutter官方網站直接的介紹是這樣寫的:

Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.

Web Hit Stable Millestone

Android和iOS是Flutter最先開始穩定支援的平台,但終究web平台還是一個觸及度最高的管道,也許平板電腦不是每個人都有,但user們還是都用電腦上班。在專案達成mobile first的任務,接著要開始擴大觸及程度時,剛好迎來Flutter Engage在3月3號發佈了Flutter 2的更新。

Perhaps the single largest announcement in Flutter 2 is production-quality support for the web.

Flutter 2正式將Web支援放進stable channel,原本要打開Web支援還得使用beta channel才行,甚至官方也建議在web正式穩定支援前,要發佈成產品都需要仔細的確認執行起來是否有bug。當下還在觀望是否該導入Flutter Web才過沒多久,隨著Flutter 2的發佈後,馬上就在隔天拿起現有的Flutter專案進行POC,直到最近成功的將原本的app正式發佈給使用者使用。

從Flutter到Flutter Web

下面我分享三個如果今天從新手Flutter開發者,一路走到開發產品等級的Flutter Web可能會發生的心路歷程與重點學習資源:

  • 從零開始開始的Flutter菜鳥

身為第一次接觸Flutter的開發者,自己覺得還不算難上手,基本上跟著官方的文件把開發環境裝起來後,接著讀完stateless和stateful widget還有column和row的layout概念後,就可以作出簡單的切版畫面。

Flutter的widget其實很多,常常都是需要的時候邊查邊實作,通常第一步會先找是不是有Flutter已經提供的widget(像是drawer、search、bottom navigation等),如果沒有的話這裡推薦pub.dev還有Flutter Gems,這兩個網站的資源可以幫你找到很多你需要的功能。最後再上http加上json serialization,或是直接跟著cookbook作大致上就能接完後端了。

  • 一個好的狀態管理很重要

成功發佈完app的1.0.0版後,接著來的就是各種app的新功能需求,在追加越來越多的跳頁功能下,開始出現很多的渲染失敗或是改一改就出現畫面亂跳頁的bug。這時候才會發現真的需要一個好的狀態管理,領悟到沒有好好規畫狀態管理的app不止難以維護,更會造成app效能變差。如果這段是在趕鴨子上架的過程被忽略掉的,這時直接建議把Flutter狀態管理這篇讀完,至少用Provider好好設計每個widget的狀態變化與重新渲染的時機。

另外要注意在取後端資料時是否有使用FutureBuilder,FutureBuilder可以幫你確認取得async資料(通常是後端的一包資料)的各種狀態,例如在接到資料前可以先拋出一個自己準備有讀取效果的widget,並在取得資料後再使用這包資料渲染出要程現的widget(對Dashboard而言可能就是一些圖表)。

  • 打開web支援要怎麼作

Flutter web在開發上其實也不困難,而且可以在VSCode用debug模式就可以直接進行開發,Web support for Flutter官方有一個篇章在作介紹,如果是專案一開始就有打算作Web平台,那建議直接跟著文件說明就能夠建立Flutter Web應用程式

但如果今天是有一個已經開發好的app產品要打開Web平台的支援,通常就會遇到比較多的阻礙,當我以為一個彈指就能把web支援打開,但遇到的就是滿滿的error訊息。要將現有的app打開web支援,通常在開發上會遇到的問題我大致上分成三種。

  1. Responsive Design(響應式設計): 不管是從手機或是平板使用的app,如果沒有對螢幕尺寸作適當的處理,通常直接放到web都會直接跑版,官方文件也有一個篇章在介紹響應式設計

  2. Cross-Platform Support Package(跨平台支援套件): 在開發過程中會有一定程度的使用第三方的package,要注意是否有支援所需要的平台特別是web,通常可以在pub.get查到。

  3. Platform Dependency Feature(平台相依性功能): 因為不同平台還是會有差異存在,有些dart自帶的library本身就不支援,比如說web就不能使用dart:io相關的函式,因此在開發的時候需要特別注意。如果需要針對不同的平台用該平台才有支援的widget,通常會先用邏輯判斷目前使用者是使用哪個平台,再渲染該平台特有的widget。

以上是自己在投入開發Flutter到打開web支援的一些心得整理,開發過程也許遇到許多不同的問題,特別是在web支援的部份遇到了許多挑戰。雖然花了許多時間解決各種遇到的轉換問題,但確實可以達到透過single codebase在web和app渲染出一樣的畫面!

上一堂講到kernel logistic regression,並證明L2-regularized linear model都可以kernel化。這堂課會嘗試將kernel放進L2-regularized linear regression作出kernel ridge regression。

因為已經知道最佳的w會是βZ的線性組合,所以這裡一樣可以將w轉換為z的線性組合,當出現Z與Z乘積時就可以換成kernel,轉成透過kernel trick解β問題。

因為這是一個無條件的最佳化問題,所以可以求梯度為0,即對β微分等於0求出讓梯度為0的β最佳解。因為K一定大於0所以可以求出需要求解的反矩陣來解出最好的β。原本之前說的要作nonlinear的方法是先作transform轉換再作regression,而且現可以另一種方法即使用kernel來達成nonlinear regression。

比較linear ridge regression和kernel ridge regression,kernel ridge rgression具有非線性性質所以有更彈性的應用來解決較複雜的問題,但其缺點就是要付出較大的計算時間複雜度。

kernel ridge regression當然也可以拿來作classification,這個方法又稱為LSSVM(least-squares SVM)。在比較Soft-Margin SVM和LSSVM,兩者得到的邊界可能會差不多,但比起Soft-Margin裡面求出的α是稀疏的,前面在解kernel ridge時β沒有特別的限制,因此β會求出dense的解,因而得到比Soft-Margin更多的support vector。而support vector較多的情況下,會造成在預測時效率較差。所以再來的重點是如何讓β也可以和標準SVM一樣求出稀疏解。

這裡介紹到Tube Regression,比起原本的regression會計算每一點算與實際值的誤差,tube regression允許在一個範圍內,即落在tube內的點可以不計算error,而落在tube外的點除了計算與實際值的誤差外要再減掉tube的寬度,得到一種新的error計算方法。這種error被稱為ε-insensitive error,下一步會嘗試透過這個新的error來解出稀疏的β值。

在比較Tube Regression和Squared Regresion會發現,兩者在預測值和實際值接近時,error計算是比較接近的。但是在相差越來越大時,squared regression的error會成長的比較快,因此也比較容易受到雜訊的影響。

L2-Regularized Ture Regression在求解時可以使用任何unconstrain的方式,和之前SVM求hinge error時一樣會遇到max無法微分,雖然也可以使用reprsenter的方式作kernel化,但卻無法保證最後求出的是稀疏解。而標準的SVM一樣無法微分,但是可以被寫成QP問題求解,再透過求對偶問題達成kernel化,而且因為滿足KKT condition可以保證其具有稀疏性。因此這裡可以把L2-Regularized Tube Regression模擬成像原本的SVM再求解(加入C並將w0拆出b值)。

再進一步加入ξn來紀錄犯錯的程度,其中為了將限制式的絕對值拆掉,會得到upper tube violation和lower tube violation兩個ξn。這個式子就是一個Support Vector Regression(SVR)的標準式,而且也會符合QP問題。

這個SVR的標準式會有兩個參數,一個是C和之前的SVM一樣用來控制regularization和violation之間的trade-off。第二個是ε用來紀錄tube的寬度決定允許的犯錯範圍。

有了SVR的primal後,就可以引入Langrange multiplier來轉成對偶問題,所以這裡會引入兩個α來對應兩個ξn,再來就是重複之前課程的推導。

因為SVM和SVR的問題相似,所以可以發現兩者的對偶很相近,而且可以透過QP來解。

回到原本的問題,我們希望β可以是稀疏的,也就是β系數在某些情況要是0。可以發現如果資料是在tube內部ξ會為0,而ξ為0的情況下complementary slackness括號內部的值就不等於0,又因為α和括號內部其中要有一邊要為0,所以α必定要為0,也就是β會等於0。所以位在tube內部的β會為0,而tube外面或是邊界就是對w有貢獻的support vector,這就可以得到β要稀疏解。

到目前為止教過的linear model總共有5種,在分類問題上,除了最早教到的PLA/pocket方法,這幾講教的linear soft-margin SVM是透過找到具有soft-margin性質的超平面來解分類問題;在regression問題中除了前面講到的linear ridge regression,還引申出透過Tube與SVM概念所延伸出來的linear SVR。另外還有regularized logistic regression結合regularization作模型複雜度修正與maximum likelihood概念來處理分類問題。

當linear model具有regularized性質時都可以引入kernel的概念,而這6講的內容分別教了如何將SVM、ridge regression、SVR和logistic regression帶入kernel trick。其中第一排的PLA/pocket和linear SVR,因為其表現都沒有第二排linear soft-margin SVM和linear ridge regression來的好,所以實務上比較少用。第三排的kernel ridge regression和kernel logistic regression因為β解出來是不是稀疏解,所以實務上傾向使用第四排的SVR。

kernel是一個power的方法強化linear model來解更複雜的問題,但要注意的是要小心處理參數的選擇以避免發生overfit。

總結這一講的內容,教到將representer theorem應用在ridge regression上,並結合tube regression引申出SVR的primal形式,再透過求解對偶得到β的稀疏解。最後則比較到目前為止教到的線性模型,並且有嘗試使用kernel來解更複雜的問題時要小心使用,避免overfit。

參考資料:
Machine Learning Techniques 6

上一堂講到在允許違反部份邊界下,引入C當作懲罰值,將Hard-Margin SVM轉成Soft-Margin。而Soft-Margin的對偶問題和Hard-Margin非常相似,只差在對偶問題中的α有上限值C。這堂課會談如果將kernel trick引入logistic regression可以怎麼作。

回顧Soft-Margin SVM,裡面發生的違反邊界會被紀錄在ξn裡面,而ξn會是1-yn(W.Zn+b),因為會紀錄下與1的距離來表式違反的程度和嚴重性;相反的在沒有違反下ξn值為0。再來可以把ξn寫成另一個更簡單的式子,即為轉換成對1-yn(W.Zn+b)和0之間取最大值來算出ξn,並帶入SVM的最佳化式子中,將ξn轉成b和w算出來的結果。

這個轉換後的最佳式其實和之前教過的regularization非常相似,就是在求長度w控制複雜度之下加上regularization項次。所以從這裡可以看到,Soft-Margin SVM其實可以從regularization推導過來,但上一講會選擇從Hard-Margin推導過來的原因是因為這個式子並不是一個QP問題,而且在兩項取最大值時也會有微分求解問題。

在前面課程推導對偶問題時有提到SVM和regularization的相似地方,而這裡可以看到Soft-Margin SVM其實就和L2 regularization是相似的,所以SVM的large margin就是一個對regularization的實現。但其中細部的差異在於Soft-Margin會使用到特別的error表示,而C的大小即為控制regularization的程度(越大的C代表越小的regularization)。

在計算zero-one error時會是一個像階梯狀的函數,因為ys是正的error為0(猜的方向正確),ys如果是負的error為1(猜的方向錯誤)。而SVM的error會由兩個線段組成,在ys離1越遠error會越大,而ys為正時error為0,SVM的error為zero-one error的上限,所以Soft-Margin也是在間接的把zeor-one error作好。

如果將SVM和logistic regression比較的話,從圖上可以發現logistic regression會和SVM很接近,進一步將ys在正無限大和負無限大比較時,兩者的error也是相近的,所以SVM其實也很像在作L2-regularized,因為在加上error項後,其實error項和logistic regression的error項非常接近。

在比較三種兩元分類方法,最一開始教的PLA需要在線性可分的情況下才好作,如果在線性不可分要透過pocket也不好達成;logistic regression的函數有很好的最佳化性質,而且加上L2-regularized後還能對模型的複雜度有一定的保護;Soft-Margin SVM差異在於最佳化使用了QP,並且在有large margin的理論保證,其error的計算方式雖然和logistic regression不同,但確實非常接近,而且兩者都是在作最佳化zero-one error的上限值。因此regularized logistic regression其實就幾乎是在作Soft-Margin SVM。

在二元分類問題中可以有兩種簡易的方法,第一種是透過SVM找到最佳的b和w,再直接丟進logistic reression作出二元分類,但這樣的手法就缺少了點logistic regression像是maximum likelihood的特色;第二種則是將SVM找到的最佳b和w,放進logistic regression當作始初解,之後再作最佳化找到最佳解,但這樣和本來logistic regression用其他初始解再找最佳解的結果相同,而且解變動後也失去了SVM的特性。

如果要同時有兩種方法的特性,可以先作SVM找到分數,再加上放縮的A與平移的B,最後再放進logistic regression訓練,其意義為先透過SVM找到最佳的超平面,再透過logistic regression作放縮與平移調整到最佳。也就是先透過SVM當作轉換,再丟進logistic regression作兩階段的的學習。

透過這樣的方式就可得到SVM的Soft binary clasifier,因為引入縮放和平移所以會和原本的SVM會不太相同,在求解logistic regression時可以使用Gradient Descent即可。到這裡我們透過kernel SVM去推論logistic regression在z空間的近似解,但這裡不是真的在z空間解出logistic regression,而且透過SVM在z空間找近似解。

複習一下SVM能使用kernel的關鍵點,是在於將W.Z的內積問題,將W轉換成一堆Z的線性組合,成為求Z.Z內積問題,這時候就能讓kernel上場。在SVM中,W就是Zn的線性組合,組合的系數就是對偶問題的解;PLA的W也是Zn的線性組合,組合的系數取決定每次犯錯的修正,而logistic regression中的W也是Zn的線性組合,這些系數來自梯度下降最終求得的結果。所以這些方法應該都能夠應用kernel trick輕易的在取得z空間的解,最好的W即可透過這些Zn來表達出來。

老師在這裡用例子證明任何的L2-regularized線性模型皆可以應用kernel。

因此,在L2-regularized的logistic regression,就可以將W替換成Zn的線性組合,從求W轉成求所有β系數。在將Zn線數組合代進入取代W後,就能將Z.Z的部份使用kernel,接著就可以透過梯度下降來求最佳解,這就是kernel logistic regression。

如果看這個kernel logistic regreaaion,先從β的角度看起來就像是在求β的線性組合,而且其中還將kernel用來作轉換與正規化;如果是從w角度來看,即為藏有kernel轉換與L2正規化的線性模型。但要注意常常β解出來會有很多0。

總結這堂課從將Soft-Margin SVM解釋成使用hinge error的regularize model,再進一步和L2-regularized logistic regresion比較兩者的相似性,透過結合兩者建立two-level learning應用在Soft Binary Classification,最後真的證明logistic regression也可以應用kernel並求出z空間的解,但會付出解出來很多β會為0的代價。

參考資料:
Machine Learning Techniques 5

上一講教了透過kernel trick來處理dual SVM中的轉換與內積,並介紹了不同的kernel function,從線性到無限多維轉換的Gaussian,但SVM還是有可能會overfit。overfit有兩種可能的原因,第一種為使用了過度複雜的轉換,第二種則是堅持要把所有資料完全的切分開來。以上面這個例子來說,如果可以容忍有錯誤分類,但可以使用簡單的線性切分也可以得到不錯的邊界;相反的,如果使用比較複雜的轉換確實完美的將資料都區分正確,反而容易造成overfit。

但該如何解決這個問題呢?之前在講pocket演算法時,pocket會找到能分錯資料最少的超平面,但hard-margin SVM除了要求要全部分對之外,還要選w長度最小的超平面。所以如果可以讓SVM也能像pocket一樣容忍一些錯誤,即不管部份分錯的點,就能達到某種程度的條件放寬。這邊引入C參數來代表放寬條件的相對重要性,C較大時代表越重視不要犯錯越好,C越小時代表越重視w長度,即分對的資料的margin越寬越好。

但放寬後的新問題將會違反QP的條件(min為二次式,條件要為一次式),而且在允許犯錯的情況下,沒有辦法分辦到底犯錯的嚴重程度(小錯或是大錯)。為了解決這兩個問題,引入ξn來紀錄離想要的值(是否靠近1)有多近,即將犯了幾個錯誤轉成犯了多大的錯誤。除了可以解決無法分辨犯錯程度的問題,也可以讓問題符合QP。

如同前面說的,C可以用來控制large margin和margin violation,越大的C代表越重視分類的正確性,而越小的C代表margin可以比較大,但會造成某些資料分錯。

當我們將Hard-Margin轉成Soft-Margin後,再來就是將它轉成對偶問題,就可以如同Hard-Margin一樣引入不同不同維度轉換的kernel function。第一步就是帶入Lagrange,有別於原本的Hard-Margin只需要帶入一個α,因為這邊分成兩個部分,因此分別帶入α和β。再進一步將ξn作微分拿掉ξ,可以發現在最佳解的位置C會等於αn+βn,就可以將βn簡化成C-αn,又因前後兩項分別都有C-αn互相消掉,所以可以再將式子簡化。

到這邊可以發現內部要解的問題其實就和之前的Hard-Margin SVM相同,只差在外面的條件帶入了C的限制,所以這邊可以和之前一樣分別對b和w作微分來得到Soft-Margin SVM的對偶問題。

再仔細看一下Soft-Margin SVM的對偶問題,可以發現和Hard-Margin SVM的對偶問題幾乎一樣,只差在現在α會有一個上限值C,而這個C是來自引入β後得到的。

當我們把Gaussian SVM用在Soft-Margin上並調整不同的C值時,可以發現最左邊當C設成1的時候,會得到一個邊界但有部份的資料是分錯的;當C一直調到到100可以發現邊界會越來越複雜,但是分錯的資料會越來越少。其中最右邊的邊界雖然可以讓所有的資料分對,但也可以造成雜訊的容忍度下降,也就是容易造成overfit,因此C和γ值需要慎選。

介紹完Soft-Margin和kernel,參數該怎麼選呢?第一個方法可以嘗試使用cross validation的方式,並帶入不同的C和γ值來找到比較好的參數組合。

另一個有趣的方法則是使用Leave-One-Out CV,SVM在使用Leave-One-Out CV的error會小於等於support vector的比例(support vector的數量除上所有樣本數),其概念為即使少了non support vector(non-SV)的點,結果並不會影響到margin的計算。

所以也可以嘗使用support vector的數量來選擇模型,因此在作cross validation前可以透過檢查support vector的數量,去掉support vector數量較多的組合作模型的安全檢查。

總結這堂課介紹了soft-margin SVM,其核心概念在於不強求把所有的類別都分對,並加上犯錯的程度大小的懲罰項。在推導的部份,Soft-Margin和Hard-Margin結果幾乎一樣,只差在Soft-Margin在αn會存在上限值C。而SVM可以將資料分成三種,分別為non-SV、存在邊界上的free-SV與可能違反邊界的SV。最後在model selection可以使用cross-validation或是參數SV的數量來作選擇。

參考資料:
Machine Learning Techniques 4

上一講說明了dual SVM,好像可以讓SVM與高維度的z空間兩者可以脫鉤,但以計算的角度,仔細看會發現在整個求解過程中的Q矩陣還是會在z空間作內積。而在z空間的內積在運算可以拆成兩個部份,一個是先透過某個函式φ轉換到z空間,第二再進行轉換後的內積。但這樣的運算方法就會變成z空間的維度作內積。

透過這個polynomial transform可以發現,其實是能先在x維度作完內積,再作函式φ的轉換,這樣就可避免產生在z空間作內積的情況,透過這個簡化的方式計算內積稱為kernel function。

回到svm不管是在計算b和最佳的超平面gsvm時,都可以將kernel function應用在計算中。這種合併函式轉換和內積計算,來避免在在高級度空間進行內積計算的方法,又稱為kernel trick。

延續前面的Poly-2 kernel例子,在每項前面再加上不同的係數,就能再對計算進一步簡化。雖然不同的函式都是轉換到相同的維度,但是會計算出不一樣的內積得到不同的距離,所以得到的幾合意義和算出來的SVM margin也會不同。其中最常使用的kernel為加上根號2與γ值的General Poly-2 kernel。

舉例來說,中間為原本的Poly-2 kernel,而左右兩個為General Poly-2 kernel但代入不同的γ值,左邊帶很小的γ右邊帶很大的γ。可以看到得出的邊界是不同的,所以得出的支撐向量也不同(方框的點)。雖然三個kernel的邊界不同,margin的定義也不同,但這邊沒辦法說哪個邊界會比較好,因為三個kernel都可以把點分開。

前面提到的Poly-2可以再加入ζ參數,就會形成一個包含3個參數的一般型態Polynomial Kernel,包含了(1)γ控制x內積算完後的放縮程度,和(2)ζ控制常數項與乘上轉換係數如何對應(3)Q代表poly作幾次方的轉換。而且不管作了幾次方的轉換,都是透遇kernel而不會在高維空間計算。使用高次方的轉換仍然可能會有overfit的風險,但SVM會透過找到最大margin來避免overfit,所以透過kernel可以讓SVM達到避免ovefit,又可以使用較複雜的模型,同時不需要高度的計算複雜度。SVM結合polynomial kernel又稱為Polynomial kernel。

Polynomial kernel如果設定成1次轉換,γ設成1,ζ設成0的話,就等於是原本的linear kernel SVM。老師建議大家在使用SVM時,可以從linear SVM開始,如果linear就可以解決問題,就不需要作高次方的nonlinear轉換。

既然可以透過kernel trick來達成高維度轉換的運算,那是否有可能作到無限多維轉換呢?老師這裡透過證明可以發現,即使在無限多維的情況,使用高斯函數作轉換是可以辦到的。

所以高斯函數也是一種kernel trick的方法,讓資料映射到無限多維的空間後找到支撐向量和超平面。但其實際上的意義為他會產生以支撐向量為中心的高斯函數的線性組合,也常被稱為RBF(Radial Basis Function) kernel。

到目前為止,SVM可以辦到使用kernel trick作無限多維的轉換後,再透過最大margin找到超平面,而SVM裡面的最大margin計算,可以對無限多維的轉換有一定程度的保護。但要注意SVM仍然會有overfit的問題,當γ越調越大時會使原本Gaussan裡面的σ變小Gaussian就會變尖(γ=1/2σ^2),所以還是要小心γ參數的選用會決定SVM的表現,通常不建議使用太大的γ值。

前面介紹到許多的kernel,不同的kernel也有其好處和壞處

  1. Linear Kernel
    最簡單的kernel,就是不作任何的維度轉換,其好處就是最簡單安全。除了QP好解之外也具有可解釋性,因為是線性所以可以透過每個特徵的權重來看出特徵的重要性,應該是遇到問題時要先被拿來嘗試使用的kernel。但其壞處在於問題如果是非線性的,那只靠線性沒辦法很好的解決問題。
  1. Polynomial Kernel
    Polynomial的好處在經過非線性轉換後,會比linear限制還要少,而且可以透過設定Q來限制模型的維度。缺點在於Polynomial有3個要給定的參數較難選定,而且在計算值時QP不容易解,尤其是γ過大的時候,因此Polynomial比較適合用在心裡有假設而且使用比較小的Q。
  1. Gaussian Kernel
    Gaussian可以作到無限多維的轉換,所以會比Linear和Polynomial使用限制更少。而且Gaussian的數值會落在0到1之間,所以數值計算難度較低;又因為Gaussian只有一個參數要給定,比起Polynomial要選3個參數,Gaussian可以更容易作參數選擇。Gaussian的壞處在於被轉換到無限多維後,模型沒辦法容易的被解讀;另外他的速度會比單純使用線性還要慢,又因為轉換較複雜,因此使用的不好會造成overfit。

Kernel表示一種特徵的相似度,但是不是任何相似度都能用來當作Kernel呢?這邊老師給出必須要滿足對稱性還有postive semi-definite兩種條件才能當作Kernel。

這一講提到了如何透過kerlnel trick快速計算轉換和內積,避開最複雜的計算,另外也比較了不同的kernel的優點和缺點。

參考資料:
Machine Learning Techniques 3