Dart 敗部復活賽:Flutter for Web

img

前言

Dart 一開始是由 Google 所推出的一種網頁程式語言,它改善了許多 JavaScript 的歷史包袱,但是很不幸的並沒有受到大眾的支持,反倒是 Microsoft 以 JavaScript 為基礎所發展的 TypeScript 因為原生支援 JavaScript,所以反而使用者日漸增長,不過 Dart 仍然持續默默的發展,在 Flutter 的加持下又重新受到大家的關注,如今 Flutter 藉由 Dart 可轉譯為 JavaScript 的特性,開始計畫性的侵蝕 Web App。

建立專案

伴隨著 Google I/O 2019 發布,Visual Studio Code 上的 Dart 與 Flutter 擴充功能也更新到 3.0 版,最大的特色就是增加 Flutter:New Web Project 指令,讓我們可以建立 Flutter 的 web 專案。

參考資料:http://dartcode.org/releases/v3-0/

我們在 VS Code 內開啟命令選擇區,並透過 Flutter:New Web Project 來建立專案。
img
一開始會需要我們輸入專案名稱,這邊筆者使用 web
img
接著選擇專案儲存路徑,這邊筆者選擇在 D:\Demo 資料夾內。
img
從 VS Code 的檔案總管可以看到專案結構跟開發 App 的專案雷同,會有一個 lib 資料夾讓我們存放所撰寫的 Dart 程式,以及一個 web 資料夾存放網頁,App 專案則是建立 androidios 資料夾。
img

開啟專案設定檔 pubspec.yaml 可以看到引用的 package 會有差異。
pubspec.yaml

pubspec.yaml
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
name: web
description: An app built using Flutter for web

environment:
# You must be using Flutter >=1.5.0 or Dart >=2.3.0
sdk: '>=2.3.0-dev.0.1 <3.0.0'

dependencies:
flutter_web: any
flutter_web_ui: any

dev_dependencies:
build_runner: ^1.4.0
build_web_compilers: ^2.0.0
pedantic: ^1.0.0

dependency_overrides:
flutter_web:
git:
url: https://github.com/flutter/flutter_web
path: packages/flutter_web
flutter_web_ui:
git:
url: https://github.com/flutter/flutter_web
path: packages/flutter_web_ui

因為我們還沒安裝過相關的 package,所以我們可以直接利用開發工具提供的快捷鈕執行安裝,當然也可以自己手動執行 flutter package get 指令。
img

執行

最後我們透過指令 webdev serve 來啟動專案。

如果沒安裝過 webdev 可以透過下列指令安裝:
flutter packages pub global activate webdev
如果無法正常執行 webdev,可以參考上一篇文章(本是同根生:Dart 開發環境)修改設定試試,或是下載 Dart SDK 來安裝。

img
透過瀏覽器開啟 http://localhost:8080,具有 Flutter App 風格的 Hello, World! 頁面就出現了。
img

測試一

開啟 lib\main.dart,可以看到程式結果幾乎完全與 Flutter App 相同,最大的差別就是 material.dart 匯入位置改變了,由原本的
import 'package:flutter/material.dart';
變更為
import 'package:flutter_web/material.dart';
img
接下來我們直接將 Flutter App 預設的範例直接覆蓋上去,並修改 material.dart 的 import 路徑。

main.dart
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// import 'package:flutter/material.dart';
import 'package:flutter_web/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

儲存 lib\main.dart 就可以發現 webdev 服務會自動編譯。
img
重新整理網頁可以發現跟 App 一樣,而且也可以正常操作。
img

Material Icons

從瀏覽器看起來整體效果一樣,但是仔細看就可以發現 icon 並沒有顯示出來,目前官方網頁也只有一頁說明,但是從 GitHub 官方範例卻可以找到說明。
flutter_web/examples/gallery/web/assets/README.md

README.md
1
2
3
4
5
Note: a reference to `MaterialIcons` is intentionally omitted because the
corresponding font is not included in this source.
If you add `MaterialIcons-Extended.ttf` to this directory, you can update
`FontManifest.json` as follows:
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
[
{
"family": "MaterialIcons",
"fonts": [
{
"asset": "MaterialIcons-Extended.ttf"
}
]
},
{
"family": "GoogleSans",
"fonts": [
{
"asset": "GoogleSans-Regular.ttf"
}
]
},
{
"family": "GalleryIcons",
"fonts": [
{
"asset": "GalleryIcons.ttf"
}
]
}
]

我們依說明在 web 資料夾內建立一個 assets 資料夾。
img
接著在其內建立一個 FontManifest.json 檔案,並加讓 MaterialIcons

FontManifest.json
1
2
3
4
5
6
7
8
9
10
[
{
"family": "MaterialIcons",
"fonts": [
{
"asset": "https://fonts.gstatic.com/s/materialicons/v42/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2"
}
]
}
]

儲存檔案並重新整理網頁就可以發現右下角的按鈕已經可以顯示圖示了。
img

測試二

接下來筆者嘗試將之前在 Flutter App 上使用 RxDart 所做的範例移植到專案內,操作畫面如下:
img
首先直接將檔案 rx.dart 複製到專案內,並修改 import 路徑。
img
rx.dart 程式碼如下:

rx.dart
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import 'dart:async';
// import 'package:flutter/material.dart';
import 'package:flutter_web/material.dart';
import 'package:rxdart/subjects.dart';

class RxPage extends StatefulWidget {
@override
_RxPageState createState() => _RxPageState();
}

final subject = BehaviorSubject<bool>.seeded(false);

class _RxPageState extends State<RxPage> {
StreamSubscription subscription;
bool isLogin = false;

@override
void initState() {
super.initState();
subscription = subject.listen((value) {
setState(() => isLogin = value);
});
}

@override
void dispose() {
subscription?.cancel();
super.dispose();
}

@override
Widget build(BuildContext context) {
List<Widget> list = <Widget>[];
if (isLogin) {
list.add(UserAccountsDrawerHeader(
accountEmail: Text("jonnyhuang@outlooj.com"),
accountName: Text("Jonny"),
currentAccountPicture: CircleAvatar(
child: Text("J"),
),
));
} else {
list.add(DrawerHeader(
decoration: BoxDecoration(
color: Colors.orange,
),
child: Text("Guest"),
));
}
list.add(ListTile(
title: Text(isLogin ? "登出" : "登入"),
trailing: Icon(Icons.exit_to_app),
onTap: () {
subject.add(!isLogin);
},
));

return Scaffold(
appBar: AppBar(
title: Text('RxDart'),
),
drawer: Drawer(
child: ListView(
children: list,
),
),
body: Page1Page(),
);
}
}

class Page1Page extends StatefulWidget {
@override
_Page1PageState createState() => _Page1PageState();
}

class _Page1PageState extends State<Page1Page> {
StreamSubscription subscription;
bool isLogin = false;
@override
void initState() {
super.initState();
subscription = subject.listen((value) {
setState(() => isLogin = value);
});
}

@override
void dispose() {
subscription?.cancel();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: isLogin ? Colors.green : Colors.grey,
alignment: Alignment.center,
child: MaterialButton(
child: Text(isLogin ? "登出" : "登入"),
onPressed: () => subject.add(!isLogin),
),
),
);
}
}

接著因為需要用到 RxDart 這個 ,所以我們開啟 pubspec.yaml 並加入 rxdart package,在 VS Code 內只要儲存 pubspec.yaml 檔,它就會自動幫我們下載 package。
img
最後修改 lib\main.dart,將 首頁(home) 換成 RxPage
img
因為有加入新的 package,所以需要重新執行 webdev serve,開啟網頁可以看到整體操作與 App 上的一樣。
img

後記

目前 Flutter for Web 還在測試階段,所以仍有可能做大幅度的調整,不過就目前進度來看可以說是大大超出筆者預期,完成度相當高,只要我們 Flutter 專案所使用的 package 本身也支援 web 移植上應該不會有太大瓶頸,當然如果 package 會使用到 Android 或 iOS 的 API 就需要額外處理,相對個其實我們現在開發 App 時就應該要順便考量到 Web 部分。