寫給前端工程師的最全、最詳細 Flutter 教程


涉及Dart語言,Flutter原理,Flutter Widget,以及Flutter中的狀態管理方案等,堪稱最全、最詳細的Flutter教程😄html

最愛折騰的就是前端工程師了,從 jQuery 折騰到 AngularJs,再折騰到 Vue、React。最愛跨端的也是前端工程師,從 phonegap,折騰到 React Native,這不又折騰到了 Flutter。圖啥?低成本地爲用戶帶來更優秀的用戶體驗。目前來講Flutter[1]多是其中最優秀的一種方案了。前端

Flutter 是什麼?

Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile[2], web[3], and desktop[4] from a single codebase.react

Flutter[5]是由原 Google Chrome 團隊成員,利用 Chrome 2D 渲染引擎,而後精簡 CSS 佈局演變而來。或者更詳細的版本android

  • Flutter 在各個原生的平臺中,使用本身的 C++的引擎渲染界面,沒有使用 webview,也不像 RN、NativeScript 同樣使用系統的組件。簡單來講平臺只是給 Flutter 提供一個畫布。
  • 界面使用 Dart 語言開發,貌似惟一支持 JIT,和 AOT 模式的強類型語言。
  • 寫法很是的現代,聲明式,組件化,Composition > inheritance,響應式……就是如今前端流行的這一套 😄
  • 一套代碼搞定全部平臺。

Flutter 爲何快?Flutter 相比 RN 的優點在哪裏?

從架構中實際上已經能看出 Flutter 爲何快,至少相比以前的當紅炸子雞 React Native 快的緣由了。ios

  • 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:git

  • RN 使用 JavaScript 來運行業務代碼,而後 JS Bridge 的方式調用平臺相關組件,性能比有損失,甚至平臺不一樣 js 引擎都不同。
  • RN 使用平臺組件,行爲一致性會有打折,或者說,開發者須要處理更多平臺相關的問題。

而具體二者的性能測試,能夠看這裏[6],結論是 Flutter,在 CPU,FPS,內存穩定上均優於 ReactNative。github

Dart 語言

在開始 Flutter 以前,咱們須要先了解下 Dart 語言…… Dart 是由 Google 開發,最初是想做爲 JavaScript 替代語言,可是失敗沉寂以後,做爲 Flutter 獨有開發語言又煥發了第二春 😂。實際上即便到了 2.0,Dart 語法[7]和 JavaScriptFlutter[8]很是的相像。單線程,Event Loop……固然做爲一篇寫給前端工程師的教程,我在這裏只想寫寫 JavaScript 中暫時沒有的,Dart 中更爲省心,也更「甜」的東西。web

  • 不會飄的 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'))

甚至能夠重寫操做符shell

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(23);
  final w = Vector(22);
  assert(v + w == Vector(45));
  assert(v - w == Vector(01));
}

注:重寫==,也須要重寫 Object hashCodegetternpm

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[9],能夠方便的使用,譬如在其它 isolate 中解析大的 json[10]

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 對此的優化看這裏[11]

Flutter 怎麼寫

到這裏終於到正題了,若是熟悉 web 前端,熟悉 React 的話,你會對下面要講的異常的熟悉。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 是可選的[12])相互嵌套,你的這個 App 就是形以下圖的一顆組件樹(Dart 入口函數的概念,main.dart -> main())。

Widget 佈局

上說過 Flutter 佈局思路來自 CSS,而 Flutter 中一切皆 Widget,所以總體佈局也很簡單:

  • 容器組件 Container
    • decoration 裝飾屬性,設置背景色,背景圖,邊框,圓角,陰影和漸變等
    • margin
    • padding
    • alignment
    • width
    • height
  • Padding,Center
  • Row,Column,Flex
  • Wrap, Flow 流式佈局
  • Stack, z 軸佈局
  • ……

更多能夠看這裏[13]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方法,不過不會有變量合併的東西,固然也有生命週期[14]能夠看到一個有狀態的組件須要兩個 Class,這樣寫的緣由在於,Flutter 中 Widget 都是 immmutable 的,狀態組件的狀態保存在 State 中,組件仍然每次從新建立,Widget 在這裏只是一種對組件的描述,Flutter 會 diff 轉換成 Element,而後轉換成 RenderObject 才渲染。Flutter Widget 更多的渲染流程能夠看這裏[15]。實際上 Widget 只是做爲組件結構一種描述,還能夠帶來的好處是,你能夠更方便的作一些主題性的組件[16], Flutter 官方提供的Material Components widgets[17]Cupertino (iOS-style) widgets[18]質量就至關高,再配合 Flutter 亞秒級的Hot Reload[19],開發體驗能夠說挺不錯的。


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());
  }

具體實現原理能夠參考這裏[20],不過 Google 封裝了一個更爲上層的庫provider[21],具體使用能夠看這裏[22]

BlOC

BlOC[23]是 Flutter team 提出建議的另外一種更高級的數據組織方式,也是我最中意的方式。簡單來講:Bloc = InheritedWidget + RxDart(Stream)Dart 語言中內置了 Steam,Stream ~= Observable,配合RxDart[24], 而後加上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[25](Rx like) + SwiftUI 也基本等於 Bloc 了。因此,Rx 仍是要趕忙學起來 😬 除去 Bloc,Flutter 中仍是可使用其餘的方案,譬如:

  • Flutter Redux [26]
  • 阿里閒魚的 Fish Redux [27]聽說性能很好
  • Mobx [28]
  • ……

展開來講如今的前端開發使用強大的框架頁面組裝已經不是難點了。開發的難點在於如何組合富交互所需的數據,也就是上面圖中的state部分。更具體來講,是怎麼優雅,高效,易維護地處理短暫數據(ephemeral state)setState()和須要共享的 App State 的問題,這是個工程性的問題,但每每也是平常開發最難的事情了,引用 Redux 做者 Dan 的一句:

「The rule of thumb is:Do whatever is less awkward[29].」

到這裏,主要的部分已經講完了,有這些已經能夠開發出一個不錯的 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 也有本身的包倉庫[30]。不過項目包的依賴使用 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<MyAppwith 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 Web, Flutter Desktop

這些還在開發當中,鑑於對 Dart 喜歡,以及對 Flutter 性能的樂觀,這些卻是很值得期待。還記得平臺只是給 Flutter 提供一個畫布麼,Flutter Desktop 將來更是能夠大有可爲 😄,相關能夠看這裏[31]。最後每種方案,每種技術都有優缺點[32],甚至技術的架構決定了,有些缺陷可能永遠都無法改進,因此 🤔


最後的最後,強烈推薦閒魚團隊的Flutter Blog[33]👍,經常拜讀,收益良多

參考資料

[1]

Flutter: https://flutter.dev/

[2]

mobile: https://flutter.dev/docs

[3]

web: https://flutter.dev/web

[4]

desktop: https://flutter.dev/desktop

[5]

Flutter: https://flutter.dev/

[6]

這裏: https://www.yuque.com/xytech/flutter/gs3pnk

[7]

Dart 語法: https://dart.dev/guides/language/language-tour

[8]

Flutter: https://flutter.dev/

[9]

compute: https://api.flutter.dev/flutter/foundation/compute.html

[10]

在其它 isolate 中解析大的 json: https://flutter.dev/docs/cookbook/networking/background-parsing

[11]

這裏: https://medium.com/dartlang/making-dart-a-better-language-for-ui-f1ccaf9f546c

[12]

new 是可選的: https://dart.dev/guides/language/language-tour#using-constructors

[13]

更多能夠看這裏: https://www.yuque.com/xytech/flutter/hc0xq7

[14]

生命週期: https://segmentfault.com/a/1190000015211309

[15]

這裏: https://www.yuque.com/xytech/flutter/tge705

[16]

主題性的組件: https://flutter.dev/docs/development/ui/widgets

[17]

Material Components widgets: https://flutter.dev/docs/development/ui/widgets/material

[18]

Cupertino (iOS-style) widgets: https://flutter.dev/docs/development/ui/widgets/cupertino

[19]

Hot Reload: https://flutter.dev/docs/development/tools/hot-reload

[20]

這裏: https://loveky.github.io/2018/07/18/how-flutter-inheritedwidget-works/

[21]

provider: https://pub.dev/packages/provider

[22]

這裏: https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple

[23]

BlOC: https://medium.com/flutterpub/architecting-your-flutter-project-bd04e144a8f1

[24]

RxDart: https://pub.dev/packages/rxdart

[25]

Combine: https://developer.apple.com/documentation/combine

[26]

Flutter Redux: https://pub.dev/packages/flutter_redux

[27]

Fish Redux: https://github.com/alibaba/fish-redux

[28]

Mobx: https://mobx.pub/

[29]

Do whatever is less awkward: https://github.com/reduxjs/redux/issues/1287#issuecomment-175351978

[30]

包倉庫: https://pub.dev/

[31]

這裏: https://github.com/flutter/flutter/wiki/Desktop-shells

[32]

優缺點: https://medium.com/asos-techblog/flutter-vs-react-native-for-ios-android-app-development-c41b4e038db9

[33]

Flutter Blog: https://www.yuque.com/xytech/flutter


後記

以上就是胡哥今天給你們分享的內容,喜歡的小夥伴記得收藏轉發,點擊在看推薦給更多的小夥伴。

胡哥有話說,一個有技術,有情懷的胡哥!現任京東前端攻城獅一枚。 

胡哥有話說,專一於大前端技術領域,分享前端系統架構,框架實現原理,最新最高效的技術實踐!

關注胡哥有話說,學習前端不迷路,歡迎多多留言交流...





喜歡本文,點個「在看」告訴我

本文分享自微信公衆號 - 胡哥有話說(hugeyouhuashuo)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索