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

EvontNg發表於2019-03-13

前言

當年React Native 正火的時候,我擼了一個一席的客戶端,最近抽空把我自己的專案用Flutter 寫一下,專案地址戳這裡,走過路過隨手給個star?,不勝感激; 以下是作為前端對Flutter 的一些看法和經驗的總結;


Dart

我在上手寫Flutter 的時候,其實一開始並沒有學習Dart,覺得有點類似TypeScript,Dart 很好上手,只在遇到一些不熟悉的問題時才去翻閱Dart文件,說一下一些不一樣的概念:

  • 變數宣告

    1. var

      在JavaScript 和Dart 中,它都可以接受任意型別,但Dart中var的變數一旦賦值,型別便會確定,則不能再改變其型別;

      var a;
      a = 'hello'; // a 已經確定為String型別
      a = 1; // 報錯,型別不能更改
      複製程式碼
    2. dynamic & Object

      javaScript中沒有dynamic 變數宣告,與var 不同,這兩個都支援宣告後改變變數型別,但Object 宣告的變數只能使用Object所擁有的屬性和方法,而dynamic 則支援所有屬性

    3. final & const

      從字面上可以看出這兩個都是宣告常量,但是const 變數是編譯時常量,而final 變數則在第一次使用時初始化;

  • 非同步支援

    在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 實戰經驗

在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 寫法的討論,對這種寫法的吐槽,最近在掘金沸點上看到一張很貼切的圖:

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

關於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 的生命週期如下圖:

一個前端碼農的 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所學到的東西,有些是查資料過程中看到的一些知識點,並沒有用在專案中,還有很多細緻的或者沒遇到過的東西值得探討,等以後遇到了有機會再講講。


參考

相關文章