寫(xiě)給前端工程師的Flutter詳細(xì)教程
最愛(ài)折騰的就是前端工程師了,從 jQuery 折騰到 AngularJs,再折騰到 Vue、React。最愛(ài)跨屏的也是前端工程師,從 phonegap,折騰到 React Native,這不又折騰到了 Flutter。
圖啥?
低成本地為用戶帶來(lái)更優(yōu)秀的用戶體驗(yàn)。
目前來(lái)說(shuō)Flutter可能是其中最優(yōu)秀的一種方案了。
Flutter 是什么?
Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.
Flutter是由原 Google Chrome 團(tuán)隊(duì)成員,利用 Chrome 2D 渲染引擎,然后精簡(jiǎn) CSS 布局演變而來(lái)。
或者更詳細(xì)的版本
- Flutter 在各個(gè)原生的平臺(tái)中,使用自己的 C++的引擎渲染界面,沒(méi)有使用 webview,也不像 RN、NativeScript 一樣使用系統(tǒng)的組件。簡(jiǎn)單來(lái)說(shuō)平臺(tái)只是給 Flutter 提供一個(gè)畫(huà)布。
- 界面使用 Dart 語(yǔ)言開(kāi)發(fā),貌似唯一支持 JIT,和 AOT 模式的強(qiáng)類型語(yǔ)言。
- 寫(xiě)法非常的現(xiàn)代,聲明式,組件化,Composition > inheritance,響應(yīng)式……就是現(xiàn)在前端流行的這一套 😄
- 一套代碼搞定所有平臺(tái)。
Flutter 為什么快?Flutter 相比 RN 的優(yōu)勢(shì)在哪里?
從架構(gòu)中實(shí)際上已經(jīng)能看出 Flutter 為什么快,至少相比之前的當(dāng)紅炸子雞 React Native 快的原因了。
- Skia 引擎,Chrome, Chrome OS,Android,F(xiàn)irefox,F(xiàn)irefox OS 都以此作為渲染引擎。
- Dart 語(yǔ)言可以 AOT 編譯成 ARM Code,讓布局以及業(yè)務(wù)代碼運(yùn)行的最快,而且 Dart 的 GC 針對(duì) Flutter 頻繁銷毀創(chuàng)建 Widget 做了專門(mén)的優(yōu)化。
- CSS 的的子集 Flex like 的布局方式,保留強(qiáng)大表現(xiàn)能力的同時(shí),也保留了性能。
- Flutter 業(yè)務(wù)書(shū)寫(xiě)的 Widget 在渲染之前 diff 轉(zhuǎn)化成 Render Object,對(duì),就像 React 中的 Virtual DOM,以此來(lái)確保開(kāi)發(fā)體驗(yàn)和性能。
而相比 React Native:
- RN 使用 JavaScript 來(lái)運(yùn)行業(yè)務(wù)代碼,然后 JS Bridge 的方式調(diào)用平臺(tái)相關(guān)組件,性能比有損失,甚至平臺(tái)不同 js 引擎都不一樣。
- RN 使用平臺(tái)組件,行為一致性會(huì)有打折,或者說(shuō),開(kāi)發(fā)者需要處理更多平臺(tái)相關(guān)的問(wèn)題。
而具體兩者的性能測(cè)試,可以看這里,結(jié)論是 Flutter,在 CPU,F(xiàn)PS,內(nèi)存穩(wěn)定上均優(yōu)于 ReactNative。
Dart 語(yǔ)言
在開(kāi)始 Flutter 之前,我們需要先了解下 Dart 語(yǔ)言……
Dart 是由 Google 開(kāi)發(fā),最初是想作為 JavaScript 替代語(yǔ)言,但是失敗沉寂之后,作為 Flutter 獨(dú)有開(kāi)發(fā)語(yǔ)言又煥發(fā)了第二春 😂。
實(shí)際上即使到了 2.0,Dart 語(yǔ)法和 JavaScriptFlutter非常的相像。單線程,Event Loop……
當(dāng)然作為一篇寫(xiě)給前端工程師的教程,我在這里只想寫(xiě)寫(xiě) JavaScript 中暫時(shí)沒(méi)有的,Dart 中更為省心,也更“甜”的東西。
- 不會(huì)飄的this
- 強(qiáng)類型,當(dāng)然前端現(xiàn)在有了 TypeScript 😬
- 強(qiáng)大方便的操作符號(hào):
- ?. 方便安全的foo?.bar取值,如果 foo 為null,那么取值為null
- ?? condition ? expr1 : expr2 可以簡(jiǎn)寫(xiě)為expr1 ?? expr2
- =和其他符號(hào)的組合: *=、~/=、&=、|= ……
- 級(jí)聯(lián)操作符(Cascade notation ..)
- // 想想這樣省了多少變量聲明
- querySelect('#button')
- ..text ="Confirm"
- ..classes.add('important')
- ..onClick.listen((e) => window.alert('Confirmed'))
甚至可以重寫(xiě)操作符
- 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));
- }
注:重寫(xiě)==,也需要重寫(xiě) 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);
- }
這點(diǎn)在 diff 對(duì)象的時(shí)候尤其有用。
lsolate
Dart 運(yùn)行在獨(dú)立隔離的 iSolate 中就類似 JavaScript 一樣,單線程事件驅(qū)動(dòng),但是 Dart 也開(kāi)放了創(chuàng)建其他 isolate,充分利用 CPU 的多和能力。
- loadData() async {
- // 通過(guò)spawn新建一個(gè)isolate,并綁定靜態(tài)方法
- ReceivePort receivePort =ReceivePort();
- await Isolate.spawn(dataLoader, receivePort.sendPort);
- // 獲取新isolate的監(jiān)聽(tīng)port
- SendPort sendPort = await receivePort.first;
- // 調(diào)用sendReceive自定義方法
- List dataList = await sendReceive(sendPort, 'https://hicc.me/posts');
- print('dataList $dataList');
- }
- // isolate的綁定方法
- static dataLoader(SendPort sendPort) async{
- // 創(chuàng)建監(jiān)聽(tīng)port,并將sendPort傳給外界用來(lái)調(diào)用
- ReceivePort receivePort =ReceivePort();
- sendPort.send(receivePort.sendPort);
- // 監(jiān)聽(tīng)外界調(diào)用
- 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);
- // 回調(diào)返回值給調(diào)用者
- callbackPort.send(dataList);
- }
- }
- // 創(chuàng)建自己的監(jiān)聽(tīng)port,并且向新isolate發(fā)送消息
- Future sendReceive(SendPort sendPort, String url) {
- ReceivePort receivePort =ReceivePort();
- sendPort.send([url, receivePort.sendPort]);
- // 接收到返回值,返回給調(diào)用者
- return receivePort.first;
- }
當(dāng)然 Flutter 中封裝了compute,可以方便的使用,譬如在其它 isolate 中解析大的 json。
Dart UI as Code
在這里單獨(dú)提出來(lái)的意義在于,從 React 開(kāi)始,到 Flutter,到最近的 Apple SwiftUI,Android Jetpack Compose 聲明式組件寫(xiě)法越發(fā)流行,Web 前端使用 JSX 來(lái)讓開(kāi)發(fā)者更方便的書(shū)寫(xiě),而 Flutter,SwiftUI 則直接從優(yōu)化語(yǔ)言本身著手。
函數(shù)類的命名參數(shù)
- void test({@required int age,String name}) {
- print(name);
- print(age);
- }
- // 解決函數(shù)調(diào)用時(shí)候,參數(shù)不明確的問(wèn)題
- test(name:"hicc",age: 30)
- // 這樣對(duì)于組件的使用尤為方便
- 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
- ];
更多 Dart 2.3 對(duì)此的優(yōu)化看這里。
Flutter 怎么寫(xiě)
到這里終于到正題了,如果熟悉 web 前端,熟悉 React 的話,你會(huì)對(duì)下面要講的異常的熟悉。
Flutter App 的一切從lib/main.dart文件的 main 函數(shù)開(kāi)始:
- 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,包括但不限于
- 結(jié)構(gòu)性元素,menu,button 等
- 樣式類元素,font,color 等
- 布局類元素,padding,margin 等
- 導(dǎo)航
- 手勢(shì)
Widget 是 Dart 中特殊的類,通過(guò)實(shí)例化(Dart 中new 是可選的)相互嵌套,你的這個(gè) App 就是形如下圖的一顆組件樹(shù)(Dart 入口函數(shù)的概念,main.dart -> main())。
Widget 布局
上說(shuō)過(guò) Flutter 布局思路來(lái)自 CSS,而 Flutter 中一切皆 Widget,因此整體布局也很簡(jiǎn)單:
- 容器組件 Container
- decoration 裝飾屬性,設(shè)置背景色,背景圖,邊框,圓角,陰影和漸變等
- margin
- padding
- alignment
- width
- height
- Padding,Center
- Row,Column,Flex
- Wrap, Flow 流式布局
- Stack, Z 軸布局
- ……
Flutter 中 Widget 可以分為三類,形如 React 中“展示組件”、“容器組件”,“context”。
StatelessWidget
這個(gè)就是 Flutter 中的“展示組件”,自身不保存狀態(tài),外部參數(shù)變化就銷毀重新創(chuàng)建。Flutter 建議盡量使用無(wú)狀態(tài)的組件。
StatefulWidget
狀態(tài)組件就是類似于 React 中的“容器組件”了,F(xiàn)lutter 中狀態(tài)組件寫(xiě)法會(huì)稍微不一樣。
- 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方法,不過(guò)不會(huì)有變量合并的東西,當(dāng)然也有生命周期。
可以看到一個(gè)有狀態(tài)的組件需要兩個(gè) Class,這樣寫(xiě)的原因在于,F(xiàn)lutter 中 Widget 都是 immmutable 的,狀態(tài)組件的狀態(tài)保存在 State 中,組件仍然每次重新創(chuàng)建,Widget 在這里只是一種對(duì)組件的描述,F(xiàn)lutter 會(huì) diff 轉(zhuǎn)換成 Element,然后轉(zhuǎn)換成 RenderObject 才渲染。
Flutter Widget 更多的渲染流程可以看這里。
實(shí)際上 Widget 只是作為組件結(jié)構(gòu)一種描述,還可以帶來(lái)的好處是,你可以更方便的做一些主題性的組件, Flutter 官方提供的Material Components widgets和Cupertino (iOS-style) widgets質(zhì)量就相當(dāng)高,再配合 Flutter 亞秒級(jí)的Hot Reload,開(kāi)發(fā)體驗(yàn)可以說(shuō)挺不錯(cuò)的。
State Management
setState()可以很方便的管理組件內(nèi)的數(shù)據(jù),但是 Flutter 中狀態(tài)同樣是從上往下流轉(zhuǎn)的,因此也會(huì)遇到和 React 中同樣的問(wèn)題,如果組件樹(shù)太深,逐層狀態(tài)創(chuàng)建就顯得很麻煩了,更不要說(shuō)代碼的易讀和易維護(hù)性了。
InheritedWidget
同樣 Flutter 也有個(gè)context一樣的東西,那就是InheritedWidget,使用起來(lái)也很簡(jiǎn)單。
- 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());
- }
具體實(shí)現(xiàn)原理可以參考這里,不過(guò) Google 封裝了一個(gè)更為上層的庫(kù)provider,具體使用可以看這里。
BlOC
BlOC是 Flutter team 提出建議的另一種更高級(jí)的數(shù)據(jù)組織方式,也是我最中意的方式。簡(jiǎn)單來(lái)說(shuō):
Bloc = InheritedWidget + RxDart(Stream)
Dart 語(yǔ)言中內(nèi)置了 Steam,Stream ~= Observable,配合RxDart, 然后加上StreamBuilder會(huì)是一種異常強(qiáng)大和自由的模式。
- 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 ?? '');
- }
- );
- }
- }
當(dāng)然 Bloc 的問(wèn)題在于
- 學(xué)習(xí)成本略高,Rx 的概念要吃透,不然你會(huì)抓狂
- 自由帶來(lái)的問(wèn)題是,可能代碼不如 Redux 類的規(guī)整。
順便,今年 Apple 也擁抱了響應(yīng)式,Combine(Rx like) + SwiftUI 也基本等于 Bloc 了。
所以,Rx 還是要趕緊學(xué)起來(lái) 😬
除去 Bloc,F(xiàn)lutter 中還是可以使用其他的方案,譬如:
- Flutter Redux
- 阿里閑魚(yú)的Fish Redux,據(jù)說(shuō)性能很好。
- Mobx
- ……
展開(kāi)來(lái)說(shuō)現(xiàn)在的前端開(kāi)發(fā)使用強(qiáng)大的框架頁(yè)面組裝已經(jīng)不是難點(diǎn)了。開(kāi)發(fā)的難點(diǎn)在于如何組合富交互所需的數(shù)據(jù),也就是上面圖中的state部分。
更具體來(lái)說(shuō),是怎么優(yōu)雅,高效,易維護(hù)地處理短暫數(shù)據(jù)(ephemeral state)setState()和需要共享的 App State 的問(wèn)題,這是個(gè)工程性的問(wèn)題,但往往也是日常開(kāi)發(fā)最難的事情了,引用 Redux 作者 Dan 的一句:
“The rule of thumb is:Do whatever is less awkward.”
到這里,主要的部分已經(jīng)講完了,有這些已經(jīng)可以開(kāi)發(fā)出一個(gè)不錯(cuò)的 App 了。剩下的就當(dāng)成一個(gè) bonus 吧。
測(cè)試
Flutter debugger,測(cè)試都是出場(chǎng)自帶,用起來(lái)也不難。
- // 測(cè)試在/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,F(xiàn)lutter,也就是 Dart 也有自己的包倉(cāng)庫(kù)。不過(guò)項(xiàng)目包的依賴使用 yaml 文件來(lái)描述:
- 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
生命周期
移動(dòng)應(yīng)用總歸需要應(yīng)用級(jí)別的生命周期,flutter 中使用生命周期鉤子,也非常的簡(jiǎn)單:
- 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 類似,F(xiàn)lutter 也是使用類似事件的機(jī)制來(lái)使用平臺(tái)相關(guān)能力。
Flutter Web, Flutter Desktop
這些還在開(kāi)發(fā)當(dāng)中,鑒于對(duì) Dart 喜歡,以及對(duì) Flutter 性能的樂(lè)觀,這些倒是很值得期待。