【Flutter】348- 寫給前端工程師的 Flutter 教程
- 2019 年 10 月 4 日
- 筆記

| 導語 最愛折騰的就是前端工程師了,從 jQuery 折騰到 AngularJs,再折騰到 Vue、React。最愛跨屏的也是前端工程師,從 phonegap,折騰到 React Native,這不又折騰到了 Flutter。
圖啥?低成本地為用戶帶來更優秀的用戶體驗。目前來說Flutter可能是其中最優秀的一種方案了。
Flutter 是什麼?
Flutter是由原 Google Chrome 團隊成員,利用 Chrome 2D 渲染引擎,然後精簡 CSS 布局演變而來。

Flutter 架構
或者更詳細的版本:

- Flutter 在各個原生的平台中,使用自己的 C++的引擎渲染介面,沒有使用 webview,也不像 RN、NativeScript 一樣使用系統的組件。 簡單來說平台只是給 Flutter 提供一個畫布。
- 介面使用 Dart 語言開發,貌似唯一支援 JIT,和 AOT 模式的強類型語言。
- 寫法非常的現代,聲明式,組件化,Composition > inheritance,響應式……就是現在前端流行的這一套 🙂
- 一套程式碼搞定所有平台。
Flutter 為什麼快?
Flutter 相比 RN 的優勢在哪裡?
從架構中實際上已經能看出 Flutter 為什麼快,至少相比之前的當紅炸子雞 React Native 快的原因了。
- Skia 引擎,Chrome, Chrome OS,Android,Firefox,Firefox OS 都以此作為渲染引擎。
- Dart 語言可以 AOT 編譯成 ARM Code,讓布局以及業務程式碼運行的最快,而且 Dart 的 GC 針對 Flutter 頻繁銷毀創建 Widget 做了專門的優化。
- CSS 的子集 Flex like 的布局方式,保留強大表現能力的同時,也保留了性能。
- Flutter 業務書寫的 Widget 在渲染之前 diff 轉化成 Render Object,對,就像 React 中的 Virtual DOM,以此來確保開發體驗和性能。
而相比 React Native:
- RN 使用 JavaScript 來運行業務程式碼,然後 JS Bridge 的方式調用平台相關組件,性能比有損失,甚至平台不同 js 引擎都不一樣。
- RN 使用平台組件,行為一致性會有打折,或者說,開發者需要處理更多平台相關的問題。
而具體兩者的性能測試,可以看這裡,結論是 Flutter,在 CPU,FPS,記憶體穩定上均優於 ReactNative。
Dart 語言
在開始 Flutter 之前,我們需要先了解下 Dart 語言。Dart 是由 Google 開發,最初是想作為 JavaScript 替代語言,但是失敗沉寂之後,作為 Flutter 獨有開發語言又煥發了第二春。
實際上即使到了 2.0,Dart 語法和 JavaScriptFlutter非常的相像。單執行緒,Event Loop……

Dart Event Loop模型
當然作為一篇寫給前端工程師的教程,我在這裡只想寫寫 JavaScript 中暫時沒有的,Dart 中更為省心,也更「甜」的東西。
- 不會飄的this
- 強類型,當然前端現在有了 TypeScript :grimacing:
- 強大方便的操作符號:
- ?. 方便安全的foo?.bar取值,如果 foo 為null,那麼取值為null
- ?? condition ? expr1 : expr2 可以簡寫為expr1 ?? expr2
- =和其他符號的組合: *=、~/=、&=、|= ……
- 級聯操作符(Cascade notation ..)
// 想想這樣省了多少變數聲明 querySelect('#button') ..text ="Confirm" ..classes.add('important') ..onClick.listen((e) => window.alert('Confirmed'))
甚至可以重寫操作符
class Vector { final int x, y; Vector(this.x, this.y); Vector operator +(Vector v) => Vector(x + v.x, y + v.y); Vector operator -(Vector v) => Vector(x - v.x, y - v.y); // Operator == and hashCode not shown. For details, see note below. // ··· } void main() { final v = Vector(2, 3); final w = Vector(2, 2); assert(v + w == Vector(4, 5)); assert(v - w == Vector(0, 1)); }
註:_重寫==,也需要重寫 Object hashCodegetter_
class Person { final String firstName, lastName; Person(this.firstName, this.lastName); // Override hashCode using strategy from Effective Java, // Chapter 11. @override int get hashCode { int result = 17; result = 37 * result + firstName.hashCode; result = 37 * result + lastName.hashCode; return result; } // You should generally implement operator == if you // override hashCode. @override bool operator ==(dynamic other) { if (other is! Person) return false; Person person = other; return (person.firstName == firstName && person.lastName == lastName); } } void main() { var p1 = Person('Bob', 'Smith'); var p2 = Person('Bob', 'Smith'); var p3 = 'not a person'; assert(p1.hashCode == p2.hashCode); assert(p1 == p2); assert(p1 != p3); }
這點在 diff 對象的時候尤其有用。
1. lsolate
Dart 運行在獨立隔離的 iSolate 中就類似 JavaScript 一樣,單執行緒事件驅動,但是 Dart 也開放了創建其他 isolate,充分利用 CPU 的多和能力。
loadData() async { // 通過spawn新建一個isolate,並綁定靜態方法 ReceivePort receivePort =ReceivePort(); await Isolate.spawn(dataLoader, receivePort.sendPort); // 獲取新isolate的監聽port SendPort sendPort = await receivePort.first; // 調用sendReceive自定義方法 List dataList = await sendReceive(sendPort, 'https://hicc.me/posts'); print('dataList $dataList'); } // isolate的綁定方法 static dataLoader(SendPort sendPort) async{ // 創建監聽port,並將sendPort傳給外界用來調用 ReceivePort receivePort =ReceivePort(); sendPort.send(receivePort.sendPort); // 監聽外界調用 await for (var msg in receivePort) { String requestURL =msg[0]; SendPort callbackPort =msg[1]; Client client = Client(); Response response = await client.get(requestURL); List dataList = json.decode(response.body); // 回調返回值給調用者 callbackPort.send(dataList); } } // 創建自己的監聽port,並且向新isolate發送消息 Future sendReceive(SendPort sendPort, String url) { ReceivePort receivePort =ReceivePort(); sendPort.send([url, receivePort.sendPort]); // 接收到返回值,返回給調用者 return receivePort.first; }
當然 Flutter 中封裝了compute,可以方便的使用,譬如在其它 isolate 中解析大的 json。
2. Dart UI as Code
在這裡單獨提出來的意義在於,從 React 開始,到 Flutter,到最近的 Apple SwiftUI,Android Jetpack Compose 聲明式組件寫法越發流行,Web 前端使用 JSX 來讓開發者更方便的書寫,而 Flutter,SwiftUI 則直接從優化語言本身著手。
3. 函數類的命名參數
void test({@required int age,String name}) { print(name); print(age); } // 解決函數調用時候,參數不明確的問題 test(name:"hicc",age: 30) // 這樣對於組件的使用尤為方便 class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Container(), floatingActionButton:FloatingActionButton() ); } }
大殺器:Collection If 和 Collection For
// collection If Widget build(BuildContext context) { return Row( children: [ IconButton(icon: Icon(Icons.menu)), Expanded(child: title), if (!isAndroid) IconButton(icon: Icon(Icons.search)), ], ); }
// Collect For var command = [ engineDartPath, frontendServer, for (var root in fileSystemRoots) "--filesystem-root=$root", for (var entryPoint in entryPoints) if (fileExists("lib/$entryPoint.json")) "lib/$entryPoint", mainPath ];
Flutter 怎麼寫
到這裡終於到正題了,如果熟悉 Web 前端,熟悉 React 的話,你會對下面要講的異常的熟悉。

UI=F(state)
Flutter App 的一切從lib/main.dart文件的 main 函數開始:
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Welcome to Flutter', home: Scaffold( appBar: AppBar( title: Text('Welcome to Flutter'), ), body: Center( child: Text('Hello World'), ), ), ); } }
Dart 類 build 方法返回的便是 Widget,在 Flutter 中一切都是 Widget,包括但不限於
- 結構性元素,menu,button 等
- 樣式類元素,font,color 等
- 布局類元素,padding,margin 等
- 導航
- 手勢
Widget 是 Dart 中特殊的類,通過實例化(Dart 中new 是可選的)相互嵌套,你的這個 App 就是形如下圖的一顆組件樹(Dart 入口函數的概念,main.dart -> main())。

Flutter Widget Tree
1. Widget 布局
上說過 Flutter 布局思路來自 CSS,而 Flutter 中一切皆 Widget,因此整體布局也很簡單:
- 容器組件 Container
- decoration 裝飾屬性,設置背景色,背景圖,邊框,圓角,陰影和漸變等
- margin
- padding
- alignment
- width
- height
- Padding,Center
- Row,Column,Flex
- Wrap, Flow 流式布局
- stack, z 軸布局
- ……
Flutter 中 Widget 可以分為三類,形如 React 中「展示組件」、「容器組件」,「context」。
2. StatelessWidget
這個就是 Flutter 中的「展示組件」,自身不保存狀態,外部參數變化就銷毀重新創建。Flutter 建議盡量使用無狀態的組件。
3. StatefulWidget
狀態組件就是類似於 React 中的「容器組件」了,Flutter 中狀態組件寫法會稍微不一樣。
class Counter extends StatefulWidget { // This class is the configuration for the state. It holds the // values (in this case nothing) provided by the parent and used by the build // method of the State. Fields in a Widget subclass are always marked "final". @override _CounterState createState() => _CounterState(); } class _CounterState extends State<Counter> { int _counter = 0; void _increment() { setState(() { // This call to setState tells the Flutter framework that // something has changed in this State, which causes it to rerun // the build method below so that the display can reflect the // updated values. If you change _counter without calling // setState(), then the build method won't be called again, // and so nothing would appear to happen. _counter++; }); } @override Widget build(BuildContext context) { // This method is rerun every time setState is called, for instance // as done by the _increment method above. // The Flutter framework has been optimized to make rerunning // build methods fast, so that you can just rebuild anything that // needs updating rather than having to individually change // instances of widgets. return Row( children: <Widget>[ RaisedButton( onPressed: _increment, child: Text('Increment'), ), Text('Count: $_counter'), ], ); } }
可以看到 Flutter 中直接使用了和 React 中同名的setState方法,不過不會有變數合併的東西,當然也有生命周期。

Flutter StatefulWidget 生命周期
可以看到一個有狀態的組件需要兩個 Class,這樣寫的原因在於,Flutter 中 Widget 都是 immmutable 的,狀態組件的狀態保存在 State 中,組件仍然每次重新創建,Widget 在這裡只是一種對組件的描述,Flutter 會 diff 轉換成 Element,然後轉換成 RenderObject 才渲染。

Flutter render object
實際上 Widget 只是作為組件結構一種描述,還可以帶來的好處是,你可以更方便的做一些主題性的組件, Flutter 官方提供的Material Components widgets和Cupertino (iOS-style) widgets品質就相當高,再配合 Flutter 亞秒級的Hot Reload,開發體驗可以說挺不錯的。
State Management
setState()可以很方便的管理組件內的數據,但是 Flutter 中狀態同樣是從上往下流轉的,因此也會遇到和 React 中同樣的問題,如果組件樹太深,逐層狀態創建就顯得很麻煩了,更不要說程式碼的易讀和易維護性了。
1. InheritedWidget
同樣 Flutter 也有個context一樣的東西,那就是InheritedWidget,使用起來也很簡單。
class GlobalData extends InheritedWidget { final int count; GlobalData({Key key, this.count,Widget child}):super(key:key,child:child); @override bool updateShouldNotify(GlobalData oldWidget) { return oldWidget.count != count; } static GlobalData of(BuildContext context) => context.inheritFromWidgetOfExactType(GlobalData); } class MyApp extends StatelessWidget { // This widget is the root of your application. @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() { _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: GlobalData( count: _counter, child: 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, ), Body(), Body2() ], ), ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), ); } } class Body extends StatelessWidget { @override Widget build(BuildContext context) { GlobalData globalData = GlobalData.of(context); return Text(globalData.count.toString()); } } class Body2 extends StatelessWidget { @override Widget build(BuildContext context) { // TODO: implement build GlobalData globalData = GlobalData.of(context); return Text(globalData.count.toString()); }
2. BlOC
BlOC是 Flutter team 提出建議的另一種更高級的數據組織方式,也是我最中意的方式。簡單來說:BlOC= InheritedWidget + RxDart(Stream)
Dart 語言中內置了 Steam,Stream ~= Observable,配合RxDart, 然後加上StreamBuilder會是一種異常強大和自由的模式。
class GlobalData extends InheritedWidget { final int count; final Stream<String> timeInterval$ = new Stream.periodic(Duration(seconds: 10)).map((time) => new DateTime.now().toString()); GlobalData({Key key, this.count,Widget child}):super(key:key,child:child); @override bool updateShouldNotify(GlobalData oldWidget) { return oldWidget.count != count; } static GlobalData of(BuildContext context) => context.inheritFromWidgetOfExactType(GlobalData); } class TimerView extends StatelessWidget { @override Widget build(BuildContext context) { GlobalData globalData = GlobalData.of(context); return StreamBuilder( stream: globalData.timeInterval$, builder: (context, snapshot) { return Text(snapshot?.data ?? ''); } ); } }
當然 Bloc 的問題在於
- 學習成本略高,Rx 的概念要吃透,不然你會抓狂
- 自由帶來的問題是,可能程式碼不如 Redux 類的規整。
順便,今年 Apple 也擁抱了響應式,Combine(Rx like) + SwiftUI 也基本等於 Bloc 了
所以,Rx 還是要趕緊學起來 :grimacing。
除去 Bloc,Flutter 中還是可以使用其他的方案,譬如:
- Flutter Redux
- 阿里閑魚的Fish Redux,_據說性能很好_。
- Mobx
- ……
展開來說現在的前端開發使用強大的框架頁面組裝已經不是難點了。開發的難點在於如何組合富交互所需的數據,也就是上面圖中的state部分。
更具體來說,是怎麼優雅,高效,易維護地處理短暫數據(ephemeral state)setState()和需要共享的 App State 的問題,這是個工程性的問題,但往往也是日常開發最難的事情了,引用 Redux 作者 Dan 的一句:
「The rule of thumb is:Do whatever is less awkward.」
到這裡,主要的部分已經講完了,有這些已經可以開發出一個不錯的 App 了。剩下的就當成一個 bonus 吧。
測試
Flutter debugger,測試都是出場自帶,用起來也不難。
// 測試在/test/目錄下面 void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(MyApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); // Tap the '+' icon and trigger a frame. await tester.tap(find.byIcon(Icons.add)); await tester.pump(); // Verify that our counter has incremented. expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); }); }
包管理,資源管理
類似與 JavaScript 的 npm,Flutter,也就是 Dart 也有自己的包倉庫。不過項目包的依賴使用 yaml 文件來描述:
name: app 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 中使用生命周期鉤子,也非常的簡單:
class MyApp extends StatefulWidget { @override _MyAppState createState() => new _MyAppState(); } class _MyAppState extends State<MyApp> with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { switch (state) { case AppLifecycleState.inactive: print('AppLifecycleState.inactive'); break; case AppLifecycleState.paused: print('AppLifecycleState.paused'); break; case AppLifecycleState.resumed: print('AppLifecycleState.resumed'); break; case AppLifecycleState.suspending: print('AppLifecycleState.suspending'); break; } super.didChangeAppLifecycleState(state); } @override Widget build(BuildContext context) { return Container(); } }
使用原生能力
和 ReactNative 類似,Flutter 也是使用類似事件的機制來使用平台相關能力。

Flutter platform channels
Flutter Web, Flutter Desktop
這些還在開發當中,鑒於對 Dart 喜歡,以及對 Flutter 性能的樂觀,這些倒是很值得期待。

Flutter Web 架構
還記得平台只是給 Flutter 提供一個畫布么,Flutter Desktop 未來更是可以大有可為。最後每種方案,每種技術都有優缺點,甚至技術的架構決定了,有些缺陷可能永遠都沒法改進。
原文來自:雲加社區 https://mp.weixin.qq.com/s/l__TJKw0DGOh01ePqAS9qw