一個前端碼農的 Flutter 實戰經驗

前言

當年React Native 正火的時候,我擼了一個一席的客戶端,最近抽空把我本身的項目用Flutter 寫一下,項目地址戳這裏,走過路過隨手給個star🌟,不勝感激; 如下是做爲前端對Flutter 的一些見解和經驗的總結;html


Dart

我在上手寫Flutter 的時候,其實一開始並無學習Dart,以爲有點相似TypeScript,Dart 很好上手,只在遇到一些不熟悉的問題時纔去翻閱Dart文檔,說一下一些不同的概念:前端

  • 變量聲明java

    1. vargit

      在JavaScript 和Dart 中,它均可以接受任意類型,但Dart中var的變量一旦賦值,類型便會肯定,則不能再改變其類型;github

      var a;
      a = 'hello'; // a 已經肯定爲String類型
      a = 1; // 報錯,類型不能更改
      複製代碼
    2. dynamic & Objectjson

      javaScript中沒有dynamic 變量聲明,與var 不一樣,這兩個都支持聲明後改變變量類型,但Object 聲明的變量只能使用Object所擁有的屬性和方法,而dynamic 則支持全部屬性canvas

    3. final & const安全

      從字面上能夠看出這兩個都是聲明常量,可是const 變量是編譯時常量,而final 變量則在第一次使用時初始化;bash

  • 異步支持網絡

    在Javascript 和Dart中都有相同用法的async、await,但沒有Promise,取而代之的是Future,但沒有resolve 和reject

  • 構造函數 在Dart 中,子類不會繼承父類的命名構造函數。若是不顯式提供子類的構造函數,系統就提供默認的構造函數。同時,寫法也變得更簡潔;

    class Point {
          num x;
          num y;
    
          Point(this.x, this.y);// 這句等同於
          /* Point(num x, num y) { this.x = x; this.y = y; } */
        }
    複製代碼
  • 箭頭函數

    在Javascript 中,箭頭函數是做爲一個影響this 做用域等的存在,但在Dart 中則是做爲縮寫語法的存在,二者的概念是不一樣的,應該區分清楚;


UI 佈局

首先咱們來看看一樣的佈局,使用HTML + CSS 和Flutter 的寫法區別

在Flutter 中,一切UI 都基於Widget,在上圖中,Container 即是一個Widget,靠style 來設置樣式(也可使用Theme,後文中細講),子類嵌套在child 中,。

class MainApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomePage(),
    );
  }
}
複製代碼

實際上這種寫法有點相似虛擬Dom,以樹形嵌套來編寫,可是這種寫法我的以爲維護起來很要命,若是沒有足夠細分組件的話,可讀性也會變得不好,實際上,Flutter 的issues 中也有關於類JSX 寫法的討論,對這種寫法的吐槽,最近在掘金沸點上看到一張很貼切的圖:

關於Widget 能夠參考Flutter 中文網的Widget 目錄,具體的我就不展開寫了,下面講講一些不常見的須要注意的問題:

  1. Expanded 不能用在不肯定或者無限高度Widget(如SingleChildScrollView) 中

  2. BuildContext 的概念

    BuildContext 其實是當前Widget 所建立的Element對象,在獲取組件尺寸,就須要用到MediaQuery.of(context).size ,路由跳轉時,也要用到Navigator.of(context),比較詳細的展開和理解說明能夠參考深刻理解BuildContext 這篇文章;

  3. Widget 的狀態管理

    這裏要介紹一下InheritedWidgetInheritedWidget是一個特殊的Widget,你能夠將其做爲另外一個子樹的父級放在Widgets樹中。該子樹的全部子Widget 都能與該InheritedWidget 公開的數據進行交互,從而實現了Widget 間的通訊;更多狀態管理的方式能夠參考 深刻探索 flutter 中的狀態管理方式

樣式

在Flutter 中,樣式並無抽離出來,而是以各類(混亂甚至有點怪異)組合的方式來使用,設置文本要用TextStyle,設置邊框背景等要用decoration,感興趣的能夠看看樣式的一些用法對比

這裏要吐槽一下樣式的管理,在Flutter 中,可使用Theme來共享樣式,可是單個Widget 的樣式除了DefaultTextStyle設置默認文本樣式外沒得繼承,仍是要本身一個個寫,這裏就推進了對組件進行細化(否則懶得重複寫),主題有如下使用方式

  • 全局主題

    new MaterialApp(
      title: title,
      theme: new ThemeData(
          brightness: Brightness.dark,
      ),
    );
    複製代碼
  • 局部主題

    new Theme(
      data: new ThemeData(
          accentColor: Colors.yellow,
      ),
      child: new Text('Hello World'),
    );
    複製代碼
  • 拓展主題

    若是你不想覆蓋全部的樣式,能夠繼承App的主題,只覆蓋部分樣式,使用copyWith方法。

    new Theme(
      data: Theme.of(context).copyWith(accentColor: Colors.yellow),
      child: new Text('extend theme'),
    );
    複製代碼
  • 獲取主題

    Theme.of(context) 會查找Widget 樹,並返回最近的一個Theme對象。若是父層級上有Theme對象,則返回這個Theme,若是沒有,就返回App的Theme。建立好主題,只要在Widget的構造方法裏面經過Theme.of(context) 方法來調用。

    new Container(
      color: Theme.of(context).accentColor,
      chile: new Text(
          'Text with a background color',
          style: Theme.of(context).textTheme.title,
      ),
    );
    複製代碼

狀態組件

Stateful 與StateLess

用過React 的都知道無狀態組件和有狀態組件,在Flutter中,StatelessWidget 即是無狀態組件,它不依賴於除了傳入的數據之外任何其餘數據,意味着改變傳入其構造函數的參數是改變其顯示的惟一方式。而StatefulWidget 則是有狀態組件,可是跟React有一點不一樣,在React 中,組件的render和state 是在一塊兒的,而Flutter 中,StatefulWidget 須要重寫createStae(),返回一個State,而build 方法須要放在State 中,至於爲何不放在StatefulWidget 呢?有兩點緣由:

  1. 狀態訪問問題

    因爲build 方法在state 每次改變時都會調用,在StatefulWidget有不少狀態時,build 方法須要傳入一個State 參數,那麼,只能將State的全部狀態公開才能在State類外部訪問,但公開狀態後,狀態將再也不具備私密性,這樣對狀態的修改將變得不可控;

    Widget build(BuildContext context, State state){
      //state.a etc...
      ...
     }
    複製代碼
  2. 繼承StatefulWidget問題

    當第一個狀況發生後,若是有個子Widget 繼承自一個引入了抽象方法build(BuildContext context)的父Widget,那麼子Widget 在實現這個build 時都須要傳入一個state,此時父Widget 就必須將本身的state 傳入給子Widget,這樣就十分不合理,由於父Widget 的state 只與自身邏輯有關,且傳遞給子Widget 還需另外的傳遞機制,所以,應該將build 方法放在State 中。

    class ChildWidgert extends ParentWidget{
         @override
         Widget build(BuildContext context, State state){
          super.build(context, _parentWidgetState)
         }
      }
    複製代碼

生命週期

Flutter 的生命週期以下圖:

說一些經常使用的:

  1. initState

    這個函數至關於在React 中的構造函數中初始化State,能夠在這一步進行數據請求加載

  2. didUpdateWidget

    當調用了 setState 改變Widget 狀態時,Flutter 會建立一個新的 Widget 來綁定這個 State 並在此方法中傳遞舊 Widget ,若是你想比對新舊 Widget 而且對 State 作一些調整,或者某些 Widget 上涉及到 controller 的變動時,就能夠在此回調方法中移除舊的 controller 並建立新的 controller;

    @override
    void didUpdateWidget(AVCycleLess oldWidget){
      super.didUpdateWidget(oldWidget);
    }
    複製代碼
  3. dispose

    當Widget 被釋放(如路由切換),Widget 中存在一些監聽或持久化的變量,你就須要在 dispose 中進行釋放。

FutureBuilder

當咱們進入頁面進行一些耗時的操做,好比請求數據、初始化某些設置等時,咱們一般須要顯示一個加載頁面,通常作法都是判斷數據狀態來切換顯示的組件,而在Flutter 中則有FutureBuilder 這種便利的解決方案,這裏展開篇幅會很長,能夠參考FutureBuilder的使用方法和注意事項

路由

在Flutter 中,路由分爲靜態路由和動態路由,靜態路由沒法傳遞參數,因此在須要傳遞參數的狀況下只能使用動態路由;

靜態路由

靜態路由在新建App 時定義,使用Navigator.of(context).pushNamed('/router/a');進行切換,pushNamed 返回一個Future,能夠接收來自下一個頁面的返回值。

return new MaterialApp(
    home: new Text('hello'),
    routes: <String, WidgetBuilder> {
        '/router/a': (_) => new APage(),
        '/router/b': (_) => new BPage(),
    },
);
// then 說明
// 當前頁面
Navigator.of(context).pushNamed('/router/b').then((value) {
    // value 爲下一個頁面的返回值
});
// b 頁面
Navigator.of(context).pop('some data');
複製代碼

動態路由

動態路由使用push方法,傳入一個route 對象,在builder 中建立一個新頁面對象,若是須要自定義動畫效果,只須要使用PageRouteBuilder 替換MaterialPageRoute ,在transitionsBuilder 中定義動畫便可。

Navigator.of(context).push(new MaterialPageRoute(builder: (_) {
    return new NewPage(data: 'some data');
}));
複製代碼

網絡請求

Dio

在Flutter 中,網絡請求是由HttpClient 進行的,但其操做十分麻煩,因此有Dio 這麼一個優秀的請求庫來簡化咱們的工做,須要注意的是,當App 只有一個數據源時,Dio 應該使用單例模式

序列化

當咱們獲取到數據時,一般咱們都會拿到一個json,在JavaScript 中,咱們能夠很任意地直接使用點操做符來獲取數據中的字段,可是在Dart中,你須要引入dart:convert,並使用JSON.decode(json),但它返回的是一個Map<String, dynamic>,意味着咱們直到運行時才知道值的類型,也就失去了大部分靜態類型語言特性:類型安全、自動補全和最重要的編譯時異常。

但這樣一來,咱們的代碼可能會變得很是容易出錯。咱們一般須要編寫模型類來序列化JSON,官方推薦了json_serializable(相關操做看這裏) 來輔助咱們生成庫序列化JSON,經過這種方式,咱們就能夠直接用點操做符來操做數據了。

若是仍是嫌麻煩,能夠試試JSONFormat4Flutter這一工具(我還沒用過,看着很不錯的樣子。)


事件處理

在Vue 中,咱們只須要使用@click 之類的方法便可監聽事件,而React 中則是onClick之類的方法,但在Flutter 中,咱們須要將須要監聽事件的元素包裹在GestureDetector 中,使用onTap 等方法來處理事件,對事件的行爲表現,咱們能夠經過設置behavior來控制,

enum HitTestBehavior {
  deferToChild, // 子widget會一個接一個的進行命中測試,若是子Widget中有測試經過的,則當前Widget經過,這就意味着,若是指針事件做用於子Widget上時,其父(祖先)Widget也確定能夠收到該事件。
  opaque,// 在命中測試時,將當前Widget當成不透明處理(即便自己是透明的),最終的效果至關於當前Widget的整個區域都是點擊區域
  translucent,// 當點擊Widget透明區域時,能夠對自身邊界內及底部可視區域都進行命中測試,這意味着點擊頂部widget透明區域時,頂部widget和底部widget均可以接收到事件
}
複製代碼

Canvas

在Flutter 中,若是須要使用Canvas,咱們須要繼承CustomPainter 並重寫paint方法來繪製自定義圖形。在使用Canvas時,咱們須要知道三個概念:

  • canvas

    畫布對象,包括了各類繪製方法,用來繪製各類圖形

  • size

    當前繪製區域的大小

  • paint

    畫筆,用來控制畫出來的各類屬性,如顏色、描邊及抗鋸齒等;

使用例子以下:

class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
      canvas.drawRect(Offset.zero & size, Paint()
      ..isAntiAlias = true // 抗鋸齒
      ..style = PaintingStyle.fill // 填充,stroke則爲使用描邊
      ..color = Color(0xFF000000) // yanse
      );
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false; // 強制不重繪,提升性能
}
複製代碼

複用

Mixin

說到mixin ,相信Vue 和React 的使用者都很熟悉,雖然React中mixin已 被高階函數或Decorator取代,但在Flutter 中,mixin 仍是得以保留。 它使用with 來引入一個mixin,定義的方式以下:

class A {
  int a = 1;
  void b(){
    print('c');
  }
}

class B with A{

}
B b = new B();
print(b.a);
b.b();

複製代碼

不過,mixin 在 Dart 中是有如下使用條件的:

  • mixins類只能繼承自object
  • mixins類不能有構造函數
  • 一個類能夠mixins多個mixins類
  • 能夠mixins多個類,不破壞Flutter的單繼承

Keep-alive

在使用Tab 時,切換Tab後,每一個Tab 都會被銷燬而後重建,因而會屢次調用initState,那有沒有相似Vue 中的<keep-alive> 組件同樣的存在呢?答案是有的,那就是AutomaticKeepAliveClientMixin。只須要繼承這個mixin並實現wantKeepAlive 方法便可。但widget在不顯示以後也不會被銷燬仍然保存在內存中,因此慎重使用這個方法

class APageState extends State<APage> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;
  // ...
}
複製代碼

後話

以上只是我這10天斷斷續續作出第一個粗糙的Flutter App所學到的東西,有些是查資料過程當中看到的一些知識點,並無用在項目中,還有不少細緻的或者沒遇到過的東西值得探討,等之後遇到了有機會再講講。


參考

相關文章
相關標籤/搜索