Flutter試用報告

高家二少爺發表於2019-02-14

目錄

一、Flutter 為何使用Dart開發語言 二、Flutter的UI系統 1.特點 2.架構簡介 2.1 Flutter Engine 2.2 Framework(Dart) 3.Flutter如何通過widget構建UI 4.Flutter是響應式的框架,但是推崇能不變就不變 5.龐大的widget體系,帶來方便的同時也帶來了高昂的學習成本 6.套娃UI程式碼,揭開一層還有一層,喝完這杯還有三杯 7.優秀的跨平臺UI框架必須要有優秀的UI除錯工具 三、Flutter與Native的交融 1.混編依賴方案的抉擇 2.通不通且看武功 2.1 打通事件通訊:平臺通道(Platform Channel) 2.2 打通跨層渲染:外接紋理(Texture)


一、Flutter 為何使用Dart開發語言

  • Dart執行時和編譯器支援Flutter的兩個關鍵特性:在開發階段採用,採用JIT模式,改動無需編譯,極大的節省了開發時間;釋出時可以通過AOT生成高效的ARM程式碼以保證應用效能。
  • 另外Dart還支援靜態型別檢查,相比JavaScript在開發時有很大優勢。
  • Flutter框架使用函式式流,這使得它在很大程度上依賴於底層的記憶體分配器,而Dart使用Chrome V8引擎來做記憶體分配,使得記憶體分配可以得到保證。
  • Dart使Flutter不需要單獨的宣告式佈局語言,如JSX或XML,或單獨的視覺化介面構建器,因為Dart的宣告式程式設計佈局易於閱讀和視覺化。所有的佈局使用一種語言,聚集在一處,Flutter很容易提供高階工具,使佈局更簡單
  • 由於Flutter應用程式被編譯為原生程式碼,因此它們不需要在領域之間建立緩慢的橋樑(例如,RN需要在JavaScript和Native之間通訊),它的啟動速度也快得多。

二、Flutter 的UI系統

1. 特點

  • Flutter不使用webView,也不使用作業系統的原生控制元件

  • Flutter使用自己的高效能渲染引擎Skia來繪製widget。 這樣不僅可以保證在Android和iOS上UI的一致性,而且也可以避免對原生控制元件依賴而帶來的限制及高昂的維護成本。

  • 組合大於繼承 控制元件本身通常由許多小型、單用途的控制元件組成,結合起來產生強大的效果,類的層次結構是扁平的,以最大化可能的組合數量

2. 架構簡介

Flutter架構圖

2.1 Flutter Engine

Flutter引擎是託管Flutter應用程式的可移植執行時。它實現了Flutter的核心庫,包括動畫和圖形、檔案和網路I/O、可訪問性支援、外掛架構以及Dart執行時和編譯工具鏈。大多數開發人員將通過Flutter框架與Flutter進行互動,該框架提供了一個現代的、可響應的框架,以及一組豐富的平臺、佈局和基礎小部件。

2.2 Framework(Dart)

這是一個純 Dart實現的 SDK,它實現了一套基礎庫,自底向上,我們來簡單介紹一下:

  • 底下兩層(Foundation和Animation、Painting、Gestures) 在Google的一些視訊中被合併為一個dart UI層,對應的是Flutter中的dart:ui包,它是Flutter引擎暴露的底層UI庫,提供動畫、手勢及繪製能力。

  • Rendering層 這一層是一個抽象的佈局層,它依賴於dart UI層,Rendering層會構建一個UI樹,當UI樹有變化時,會計算出有變化的部分,然後更新UI樹,最終將UI樹繪製到螢幕上,這個過程類似於React中的虛擬DOM。Rendering層可以說是Flutter UI框架最核心的部分,它除了確定每個UI元素的位置、大小之外還要進行座標變換、繪製(呼叫底層dart:ui)。

  • Widgets層是Flutter提供的的一套基礎元件庫 在基礎元件庫之上,Flutter還提供了 Material 和Cupertino兩種視覺風格的元件庫。而我們Flutter開發的大多數場景,只是和這兩層打交道。

在Flutter中,幾乎一切都是widget。應用程式,頁面,佈局,檢視,事件,通知,甚至是具體的文字樣式。統一化為widget的方式,使得Flutter的程式碼更加統一。

Flutter的widget是對頁面UI的一種描述,類似於web中的html,iOS中的xib,android中的xml。Flutter在構建UI過程中也是形成一個widget樹,就如iOS的檢視樹。但是不同的是這個樹並不是最終渲染的樹。

3. Flutter如何通過widget構建UI

先來看一下Flutter的渲染管道:

Rendering Pipeline

在這個渲染過程中經歷了widget樹轉化成element樹再到最終渲染的renderObject樹的過程,如下:

樹的轉化

  • Widget:存放渲染內容、檢視佈局資訊,widget的屬性最好都是immutable(如何更新資料呢?檢視後續內容)

  • Element:存放上下文,通過Element遍歷檢視樹,Element同時持有Widget和RenderObject

  • RenderObject:根據Widget的佈局屬性進行layout,paint Widget傳人的內容

element相比於widget增加了上下文的資訊。element是對應widget,在渲染樹的例項化節點。同一個widget可以對應渲染樹中的多個element,就像是一個檢視模板。

widget都是不可變的,初始狀態設定以後就不可再變化。也就是說,每次檢視的更新都會重新構建widget本身和子widget(具體表現為重新執行widget的build方法)。

針對檢視在執行時可能變化的情況,Flutter引入了State來管理檢視的狀態。在修改資料之後,需要主動呼叫setState()來觸發檢視狀態的更新。不像普通的雙向繫結,資料一修改就會觸發檢視的變化,容易造成檢視在短時間內多次更新渲染。從這裡也能看得出Flutter的設計者並不希望你頻繁地去更新檢視狀態,畢竟重新構建widget樹的代價也是蠻大,尤其是相對複雜的頁面。

另外,Flutter在檢視描述widget和真實渲染的RenderObject的中間設計的Element層,對某一時刻的事件做了彙總和比對,只對真正需要修改的部分同步到真實渲染的RenderObject樹上面,做到最小程度的修改,以提高渲染效率。

4. Flutter是響應式的框架,但是推崇能不變就不變

擁有響應式框架的以下特點

  • 不直接操作UI,改為通過修改資料然後更新檢視的狀態來驅動檢視變化
  • 通過檢視事件的繫結來運算元據並最終將結果反作用於檢視

Flutter在頁面渲染上面的核心思想是simple is fast,所以相對於可變狀態的StatefulWidget還設計了狀態不可變的StatelessWidget,也就更加強調了能不變就不變的理念。

5. 龐大的widget體系帶來方便的同時也帶來了高昂的學習成本

Flutter有一個龐大的元件體系,有很多iOS風格(Cupertino)和安卓風格(Material)的現成widget可以使用,使得UI的構建變得相對容易。但是,龐大的元件體系帶來方便的同時也帶來了高昂的學習成本(單單記下這些widget的大體功能都要花不少時間)。

不過值得慶幸的是,你常用的widget並不會這麼多。上面說過widget只是介面描述,同一個介面實現的方式都會有很多種,每個人都會使用自己熟悉和擅長的方式去構建介面,但是經過轉換成Element樹,最終到達的RenderObject樹可能是一樣的。有一種殊途同歸的感覺。

下圖是Flutter常見的widget,體會一下吧。

flutter widgets (圖片來自網上)

6. 套娃UI程式碼,揭開一層還有一層,喝完這杯還有三杯

由於Flutter基本上都是由widget實現,所以也就難以避免一層套一層的程式碼風格,頗有HTML風範。

  • 控制元件套一層
  • 容器修飾套一層(圓角,著色等)
  • 事件套一層
  • 佈局套N層
  • 父級控制元件套N層 ……
  • 頁面也來套一層

有些抽象,我們來看個實際的例子。

實現這樣的一個cell

以下是Cell的檢視程式碼:

        Column(//縱向分欄
            children: <Widget>[
                Padding(//邊距
                  padding: EdgeInsets.all(10),
                  child: Row(//橫向分欄
                    children: <Widget>[

                      ClipRRect(//切圓角
                        borderRadius: BorderRadius.circular(10.0),
                        child: Image.asset('images/icon.png',width: 80,height: 80),
                      ),

                      Padding(//邊距
                        padding: const EdgeInsets.only(left: 10),
                        child: Column(//縱向分欄
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: <Widget>[
                            Text(//文字控制元件
                              '香草拿鐵',
                              style: TextStyle(//文字風格
                                  fontSize: 18,
                                  color: Colors.black
                              ),
                            ),

                            Text(
                              'Vanilla Latte',
                              style: TextStyle(
                                  fontSize: 14,
                                  color: Color(0xffcccccc)
                              ),
                            ),

                            Text(
                              '預設:大/單糖/熱',
                              style: TextStyle(
                                  fontSize: 14,
                                  color: Color(0xffcccccc)
                              ),
                            ),

                            Text(
                              '¥27',
                              style: TextStyle(
                                  fontSize: 17,
                                  color: Colors.black
                              ),
                            ),

                          ],
                        ),
                      ),
                      Expanded(//充滿父容器剩餘空間
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.end,//右對齊
                          children: <Widget>[
                            GestureDetector(
                              onTap: (){//點選事件觸發
                              },
                              child: ClipRRect(//切圓角
                                borderRadius: BorderRadius.circular(10.0),
                                child: Container(//容器修飾,用於新增藍色背景
                                    color:Colors.blue ,
                                    child: Icon(//圖示
                                      Icons.add,
                                      size: 20,
                                      color: Colors.white
                                    )
                                ),
                              ),
                            )
                          ],
                        ),
                      )
                    ],
                  ),
                ),
                Container(
                    color: Color(0xffcccccc),
                    height: 0.5
                )
              ],
            );

複製程式碼

在IDE中有個輔助線和尾部的備註會稍微好一點,但是已經習慣iOS程式碼風格的筆者確實有些適應不過來(雖然已經磨合一段時間了):

image.png

也許,你和我一樣想到了封裝,好,我們就封裝一下。

//cell整體
Column(
  children: <Widget>[
    getContent(),/*cell內容*/
    getBottomLine()/*分割線*/
  ],
);

/*cell內容*/
Widget getContent(){
  return Padding(
    padding: EdgeInsets.all(10),
    child: Row(
      children: <Widget>[
        getHeadIcon(),/*頭像*/
        Padding(/*中間文字列*/
          padding: const EdgeInsets.only(left: 10),
          child: getMiddleWidget(),
        ),
        Expanded(/*充滿父容器剩餘空間*/
            child: getRightButton()/*按鈕*/
        )
      ],
    ),
  );
}

/*分割線*/
Widget getBottomLine(){
  return Container(
      color: Color(0xffcccccc),
      height: 0.5
  );
}

/*頭像*/
Widget getHeadIcon(){
  return ClipRRect(//切圓角
    borderRadius: BorderRadius.circular(10.0),
    child: Image.asset('images/icon.png',width: 80, height: 80),
  );
}

/*中間的文字列*/
Widget getMiddleWidget() {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: <Widget>[
      getText('香草拿鐵', 18, Colors.black),
      getText('Vanilla Latte', 14, Color(0xffcccccc)),
      getText('預設:大/單糖/熱', 14, Color(0xffcccccc)),
      getText('¥27', 17, Colors.black)
    ],
  );
}

/*右側按鈕*/
Widget getRightButton(){
  return Row(
    mainAxisAlignment: MainAxisAlignment.end,//右對齊
    children: <Widget>[
      GestureDetector(
        onTap: (){//點選事件觸發

        },
        child: ClipRRect(//切圓角
          borderRadius: BorderRadius.circular(10.0),
          child: Container(
              color:Colors.blue ,
              child: Icon(
                  Icons.add,
                  size: 20,
                  color: Colors.white
              )
          ),
        ),
      )
    ],
  );
}

/*文字*/
Text getText(text, double fontSize, Color color){
  return Text(
    text,
    style: TextStyle(
        fontSize: fontSize,
        color: color
    ),
  );
}
複製程式碼

以上已經是較為詳細的封裝了,當然因為層級很多,要再繼續拆分下去也不是不可以,這就涉及封裝粒度以及封裝的最終成果能不能成正比了。看一下結構:

  • cell整體
  • cell內容
    • 頭像
    • 文字列
      • 文字
    • 按鈕
  • 分割線

以上,拆分後相對好一些,但是一層套一層的詬病還是無法避免。 (比如,cell內容這個函式,右側按鈕這個函式不考慮,因為這個是非常規的按鈕實現方式,一般用Flutter的按鈕widget即可)

筆者認為,造成這個結果的最主要原因即是其最大的特點:一切皆widget,widget包widget。

如果要讓程式碼夠優雅,佈局粒度的劃分是值得思量的。

7.優秀的跨平臺UI框架必須要有優秀的UI除錯工具

在Flutter Inspector之中提供了,視覺化檢視樹檢視工具,雖然與xcode的 介面除錯工具相比是2D的有些遺憾,不過也已經挺強大。如下圖:

Flutter Inspector select widget mode

更多工具
  • 效能監控(Performance Overlay)可以檢視GPU和UI的幀率

    Performance Overlay

  • 繪製基線(Paint Baselines)

    image.png

  • Debug Paint 展示所有控制元件的繪製邊界 綠色箭頭表示可滾動內容,以及可滾動內容的初始到結束的方向

    Paint

因為widget樹並不是最終繪製的UI樹,所以Flutter 監察器還提供了真正的繪製樹檢視工具,如下Render Tree分欄。

Render Tree

Flutter新版本外掛(32.0.1)提供了程式碼的同步定位功能,越來越好了呢

程式碼同步定位

這些工具在平常的開發中已經夠用,其他細節留給讀者們自己探索吧。

三、Flutter 與原生互動

1.混編依賴方案的抉擇

1.1 Flutter預設的工程構建方式,native工程完全是Flutter構建產物

Flutter工程目錄
Flutter預設
預設的方式,無法在已有原生工程的基礎上引入Flutter,必須完整重新建立整個工程,這個是致命的。問題還有很多,比如:

  • native反向依賴Flutter父目錄,耦合嚴重
  • 程式碼庫難以拆分管理
  • 對純native開發的團隊成員造成入侵,需要完備的Flutter開發環境,和相應的構建步驟

1.2 三個程式碼庫獨立,修改 Flutter 構建流程將構建產物直接提供給native作為依賴

本地依賴

這個方式中,以iOS為例,將Flutter.framework及相關外掛等做成本地的pod依賴,資源也複製到本地進行維護。這樣Flutter就被打包成了pod庫, 在native團隊成員那邊,Flutter就是黑盒,只管用就行了。Flutter pod庫的引用內容需要各個團隊成員走Flutter構建流程去生成。雖說程式碼倉庫比較好分開了,但是缺點還是有的:

  • 需要對Flutter原有的構造流程進行稍嫌複雜的改動
  • Native工程與Flutter的內容還是耦合在本地
  • Native開發者仍然需要完備的Flutter開發環境

1.3 將Flutter本地依賴修改成遠端依賴,native開發完全脫離Flutter

這個方案是將Flutter所有依賴內容都放在獨立的遠端倉庫中,native如同引用公開三方庫一樣去引用Flutter。這時候,native就不需要Flutter開發環境了。 要說這個方案的缺點就是同步的流程變得更繁瑣,Flutter內容的變動需要先同步到遠端倉庫再同步到native依賴。極端情況,在native與Flutter頻繁互動的時候,就需要頻繁更新依賴庫。Flutter依賴庫的版本和native程式碼版本的對應管理也是需要額外耗費精力。不過,這個不算大問題,與以往的H5與原生的混編類似,沿用即可。

Flutter遠端依賴

2 通不通且看武功

Flutter基於SKIA使用Dart搭建了自己的UI框架,而底層最終都是呼叫OpenGL繪製,在Native和Flutter Engine上實現了UI的隔離。那麼開發者書在寫UI程式碼時就不用再關心平臺實現,從而實現了跨平臺。這層隔離在Flutter Engine和Native之間豎立了一座大山,想要實現通訊就得另闢蹊徑。

2.1 打通事件通訊:平臺通道(Platform Channel)

Flutter與原生之間的通訊依賴靈活的訊息傳遞方式:平臺通道(Platform Channel)。 平臺指的就是指Flutter執行的平臺,如Android或IOS,可以認為就是應用的原生部分,平臺通道正是Flutter和原生之間通訊的橋樑。

平臺通道訊息傳遞
當在Flutter中呼叫原生方法時,呼叫資訊通過平臺通道傳遞到原生,原生收到呼叫資訊後方可執行指定的操作,如需返回資料,則原生會將資料再通過平臺通道傳遞給Flutter。值得注意的是訊息傳遞是非同步的,這確保了使用者介面在訊息傳遞時不會被掛起。

平臺通道的能力

  • 傳遞小量資料:基本資料型別,陣列,字典,二進位制資料;
  • 通過定製可傳遞大資料塊,但是用於如影象,視訊等大資料的傳輸必然引起記憶體和CPU的巨大消耗
  • 非執行緒安全,native的回撥必須在主執行緒執行,故應該在Native端的Handler中處理耗時操作

平臺通道的設計初衷並不是用來傳遞大資料的,從本質上說是提供了一個訊息傳送機制。

2.2 打通影象渲染:外接紋理(Texture)

紋理(Texture):可以理解為GPU內代表影象資料的一個物件。 Flutter提供了一個Texture控制元件,這個控制元件上顯示的資料,需要由Native提供。

image.png
Flutter和Native傳輸的資料載體是PixelBuffer,Native端的資料來源(攝像頭、播放器等)將資料寫入PixelBuffer,Flutter拿到PixelBuffer以後轉成OpenGLES Texture,交由Skia繪製。 通過這個方式,Flutter就可以容易的繪製出一切Native端想要繪製的資料,除了攝像頭播放器等動態影象資料,也給其他諸如地圖等檢視的展示提供了另一種可能。

相關文章