寫給前端工程師的 Flutter 教程

小刀c發表於2019-08-05

首發cc log?


最愛折騰的就是前端工程師了,從 jQuery 折騰到 AngularJs,再折騰到 Vue、React。 最愛跨端的也是前端工程師,從 phonegap,折騰到 React Native,這不又折騰到了 Flutter。

圖啥?

低成本地為使用者帶來更優秀的使用者體驗

目前來說Flutter可能是其中最優秀的一種方案了。

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 團隊成員,利用 Chrome 2D 渲染引擎,然後精簡 CSS 佈局演變而來。

Flutter 架構

或者更詳細的版本

寫給前端工程師的 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 ?
  • 強大方便的操作符號:
    • ?. 方便安全的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 物件的時候尤其有用。

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

Dart UI as Code

在這裡單獨提出來的意義在於,從 React 開始,到 Flutter,到最近的 Apple SwiftUI,Android Jetpack Compose 宣告式元件寫法越發流行,Web 前端使用 JSX 來讓開發者更方便的書寫,而 Flutter,SwiftUI 則直接從優化語言本身著手。

函式類的命名引數

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
];
複製程式碼

更多 Dart 2.3 對此的優化看這裡

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

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”。

StatelessWidget

這個就是 Flutter 中的“展示元件”,自身不儲存狀態,外部引數變化就銷燬重新建立。Flutter 建議儘量使用無狀態的元件。

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

Flutter Widget 更多的渲染流程可以看這裡

實際上 Widget 只是作為元件結構一種描述,還可以帶來的好處是,你可以更方便的做一些主題性的元件, Flutter 官方提供的Material Components widgetsCupertino (iOS-style) widgets質量就相當高,再配合 Flutter 亞秒級的Hot Reload,開發體驗可以說挺不錯的。


State Management

setState()可以很方便的管理元件內的資料,但是 Flutter 中狀態同樣是從上往下流轉的,因此也會遇到和 React 中同樣的問題,如果元件樹太深,逐層狀態建立就顯得很麻煩了,更不要說程式碼的易讀和易維護性了。

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());
  }
複製程式碼

具體實現原理可以參考這裡,不過 Google 封裝了一個更為上層的庫provider,具體使用可以看這裡

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 還是要趕緊學起來 ?

除去 Bloc,Flutter 中還是可以使用其他的方案,譬如:

展開來說現在的前端開發使用強大的框架頁面組裝已經不是難點了。開發的難點在於如何組合富互動所需的資料,也就是上面圖中的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 未來更是可以大有可為 ?,相關可以看這裡

最後每種方案,每種技術都有優缺點,甚至技術的架構決定了,有些缺陷可能永遠都沒法改進,所以 ?


最後的最後,強烈推薦閒魚團隊的Flutter Blog?,常常拜讀,收益良多。

相關文章