【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