每個 Flutter 開發者都應該知道的框架總覽

語言: CN / TW / HK

theme: fancy

這是我參與2022首次更文挑戰的第1天,活動詳情檢視:2022首次更文挑戰

本篇文章翻譯自官方的👉總覽文章,這篇文章基本上把 Flutter 介紹清楚了,如果想從總體上知道 Flutter 是咋回事,本篇文章是最好的教程了。

以下是正文


本文旨在從高層級提供一個 Flutter 框架結構的總體概覽,介紹一些其設計上的核心原則和概念。

Flutter 是一個跨平臺的 UI 工具包,目的是一份程式碼可以執行在不同的作業系統上,比如 Android、IOS等等,同時也可以讓應用直接和底層的平臺服務互動。我們的目標是:儘量用一份程式碼,開發者就可以在不用的平臺上開發出高效能、高保真的 APP。擁抱差異,更少程式碼,更高效能

在開發階段,Flutter 執行在虛擬機器上,虛擬機器提供了 hot reload 的功能,可以載入每次開發者改動的差異程式碼,而不需要全程式碼的編譯。在正式版本上,Flutter 應用是直接編譯成了機器碼:Intel x64、ARM、JavaScript等等。這裡說的開發階段和正式版本是指 Flutter 產物的模式,Flutter 的產物有三種模式 debug、release、profile。Flutter 的 framework 是開源的,開源的協議是 BSD 協議,並且有活躍繁榮的第三方庫社群,這些優秀的第三方庫很好的補充了 Flutter 的能力。

本文的總覽分為以下幾個部分:

  1. 分層模型: Flutter 的組成部分
  2. 響應式 UI : Flutter UI 開發的核心概念
  3. Widgets 介紹: Flutter UI 程式碼構建的基礎
  4. 渲染流程: Flutter 是如何將 UI 程式碼轉化為螢幕畫素點的
  5. 平臺嵌入 的總覽: 讓移動端和桌面系統執行 Flutter 應用
  6. 用其他程式碼整合 Flutter: 介紹 Flutter 可用的不同的技術資訊
  7. Web 的支援: 總結 Flutter 在瀏覽器環境中的特點

框架分層

從設計上來看,Flutter 框架是可擴充套件的、分層的。Flutter 由一系列的單獨的依賴包組成,而且這些依賴包依賴底層。上層沒有許可權訪問下層,並且框架層的每個部分都是可插拔的。

image.png

對於底層的作業系統來說,Flutter 應用被打成的應用包和其他的 Native 應用是一樣。平臺特定的嵌入層提供了一個入口:協調底層作業系統來訪問一些底層的服務,比如渲染桌面、可訪問性、輸入等,管理事件迴圈。這個嵌入層是被特定的平臺語言開發的,Android 系統是 Java 和 C++,iOS 和 macOS 系統是 Objective-C/Objective-C++,Windows 和 Linux 系統是 C++。正是由於這一層的存在,Flutter 程式碼可以整合進已經存在的應用,也可以直接使用 Flutter 程式碼打包整個應用。Flutter 為通用的平臺提供了一些嵌入器,其他的嵌入器也是存在的

Flutter 的核心是 Flutter engine,engine 是 C++ 開發的,並且 Flutter 應用提供最 原始的支援,比如協議、通道等等。當新的一幀需要被繪製的時候,Flutter engine 就會柵格化程式碼合成的繪製資訊。Engine 也為上層封裝了訪問底層的 API:圖形影象化、文字佈局、檔案和網路 I/O、訪問性支援、外掛架構、Dart執行時、Dart編譯工具鏈等等。

Flutter engine 暴漏是通過 dart:ui 這個庫來暴漏給上一層的,這個庫用 Dart 類包裝了底層的 C++ 程式碼。像上面說的 engine 的功能,這個庫包含了驅動輸入、圖形化、文字渲染系統等功能。

Typically, developers interact with Flutter through the Flutter framework, which provides a modern, reactive framework written in the Dart language. It includes a rich set of platform, layout, and foundational libraries, composed of a series of layers. Working from the bottom to the top, we have: 一般來說,開發者通過 Flutter framework 來和 Flutter 互動,這一層是 Dart 程式碼,提供了現代的、響應式的 Flutter 框架。這一層包括了和平臺、佈局、基礎相關的庫,並且也是分層的,自底向上以次有:

  • 必要的基礎類以及常用的底層程式碼塊的抽象,比如動畫繪製手勢

  • 處理佈局的rendering layer,在這一層,可以構建一棵渲染物件的節點樹,你也可以動態的操作這些節點,那麼佈局就會自動響應你的改變。

  • 合成抽象的 widgets layer,渲染層的每一個渲染物件在 Widget 層都會有一個 Widget 物件與之對應。另外,在這一層開發者也可以定義一些可以複用的組合類,就是這這一層引入了響應式框架。

  • [Material] 和 [Cupertino] 庫, 提供了全套的 Material和 iOS 風格的原始元件。

Flutter 框架是相對來說比較小的,一些開發者用到的高階功能大多是以包的形式實現的,比如像 camerawebview 這樣的平臺外掛,像 charactershttpanimations 這樣的平臺無關的包,平臺無關的包可以完全依賴 Dart 和 Flutter依賴。這些高階包有一些是生態共建的,比如支付、蘋果證書、動畫等等。

下面就從響應式 UI 程式設計以此向下層展開描述。主要內容有,介紹 Widget 是怎麼結合到一起的,Widget 是怎麼轉化為渲染物件的,介紹 Flutter 是怎麼整合以及互操作平臺程式碼的,最後簡要總結Flutter 的 Web支援。

響應式 UI

總體上來說,Flutter 是一個響應式的非宣告式的UI 框架,這意味著開發者僅僅需要提供程式狀態與程式 UI 的對映,框架會在應用狀態改變的時候自動完成 UI 的重新整理。這種設計的靈感得益於 Facebook 的 React 框架,React 框架對很多傳統的設計原則進行了重新思考。

在大多數傳統的 UI 框架中,UI 的初始化狀態只被描述一次,然後為了響應各種事件會單獨的更新。這種方法的一個痛點是,隨著應用複雜性的增長,開發者需要時刻注意狀態的改變是怎麼層疊地貫穿整個 UI 的。比如,考慮下面的 UI:

image.png

上面有許多狀態可以改變的地方:Color box、色帶 Slider、Radio按鈕等等。只要使用者和 UI 互動,那麼改變必須被響應到每一個地方。更麻煩的是,頁面一個很小的改動,比如拖動一下色帶,可能會導致一系列連鎖的反應,進而影響到很多看似不相干的程式碼。比如色帶的拖動,文字框裡面也要改變。

一種解決的方案是像 MVC 這樣的程式碼開發架構,通過 controller 將資料的改變推送到 model,然後,model 通過 controller 將新的狀態 推送給 view。但是,這樣的方式其實也是有瑕疵的,因為建立和更新 UI 元素是兩個單獨的步驟,可能會導致不同步。

沿著其他響應式框架的腳步👣,Flutter 通過 UI 與底層狀態徹底的解耦來解決這個問題。在響應式的 API 開發背景下,開發者僅僅建立 UI 的描述,framework 會在執行時使用我們的描述建立或者更新介面。

在 Flutter 中,我們所說的元件是 Widget,並且 Widget 是不可變的,可以形成 Widget 樹形結構。這些元件用於管理獨立的佈局物件樹,佈局樹用與管理獨立的合成物件樹。Widget 樹到佈局樹再到合成樹。Flutter的核心就是,確保可以有效的修改樹中部分節點:把上層樹轉化成低層級的樹(Widget到佈局),並且在這些樹上傳遞改變。

在Flutter中,小部件(類似於React中的元件)由用於配置物件樹的不可變類表示。這些小部件用於管理用於佈局的獨立物件樹,然後用於管理用於合成的獨立物件樹。Flutter的核心是一系列機制,可以有效地在樹的修改部分行走,將物件樹轉換為較低階的物件樹,並在這些樹之間傳播變化。

開發者需要在 Widget 的 build() 方法中將狀態轉化為 UI:

dart UI = f(state)

在 Flutter 設計中,build() 方法執行起來會很快,並且沒啥副作用,framework 會在需要呼叫的時候呼叫它。

這種響應式的框架需要一些特定的語言特徵(物件快速例項化和刪除),而 Dart 就很符合幹這件事

Widgets

正如前面所提,Flutter 著重強調 Widget 是合成的一個單元。Flutter 應用的 UI 就是 Widget 構建塊,並且每一個 Widget 都是一份不可變的描述。

Widget 在組合的基礎上形成一個體繫結構。每一個 Widget 都巢狀在它的父節點裡面,並且從父節點接受上下文。這種結構一直延伸到根 Widget,像下面的簡單程式碼:

```dart import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key);

@override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text('My Home Page'), ), body: Center( child: Builder( builder: (BuildContext context) { return Column( children: [ const Text('Hello World'), const SizedBox(height: 20), ElevatedButton( onPressed: () { print('Click!'); }, child: const Text('A button'), ), ], ); }, ), ), ), ); } } ``` 在這個程式碼中,所有的類都是 Widget。

使用者互動的時候會生成事件,App會更新 UI 來響應事件。更新的方式是告訴 framework 用另一個 Widget 來替換 Widget。framework 就會比較 新的和舊的 Widget,並且有效的更新 UI。

Flutter 有其自己的實現機制來控制 UI,而不是按照系統所提供的方式:比如說,這段程式碼是iOS Switch controlAndroid control純 Dart 的實現。

這種方式有以下幾點好處:

  • 無限的可擴充套件性。比如開發者想要一個 Switch 元件,那麼可以用任意的方式來建立,不需要侷限於作業系統所提供的

  • 避免效能瓶頸。這種方式執行 Flutter 一次就合成整個螢幕資訊,而不需要在 Flutter 程式碼和 平臺程式碼之間來回切換

  • 將應用的執行和作業系統的依賴解耦。Flutter 應用在作業系統的所有版本上執行的效果是一樣的,即使作業系統改變了他自己的一些控制元件實現。

組合先與繼承

Widget 通常由許多更小的 、用途更單一的 Widget 組成,組合起來的 Widget 往往可以產生更加有力的效果。

理想的效果,設計上概念的數量儘可能少,然而實際的總量表要儘可能大。比如說,在 Widget 層,概念只有一個,那就是 Widget,表示螢幕繪製、佈局、使用者互動、狀態管理、主題定製、動畫、路由導航等等。在動畫層,只有兩個概念:Animation 和 Tween。在渲染層,只有一個概念 RenderObject,用於描述佈局、繪製、點選、可訪問。而這些層級中,每一層都有大量的具體實現來具化概念,比如有幾百個 widget 和 render物件,幾十個動畫和插值型別。

Flutter 有意地將類的繼承體系設計的淺而寬,目的是最大化組合的數量。每一個小粒度的 可組合的Widget儘量聚焦做好自己的功能。核心的功能是抽象的,即使是像間距、對齊這樣的基礎功能,也被設計成具體的 Widget,而不是把這些功能新增到基礎的 Widget 中。因此,假如我們想要居中一個元件,不是新增 Align 屬性,而是使用 Center 元件包裹。

間距、對齊、橫向排列、豎向排列,表格等等都是 Widget,佈局型別的 Widget 並沒有它自己本身可視的樣子。但是呢,它們就是控制其他元件的佈局。Flutter 也包括了一些功能性元件,這些功能元件也利用這種組合的方法。

比如,Container 是非常常用的元件,它本身是有負責佈局、繪製、定位、尺寸的幾個小 Widget 組成,具體來說,Container 由 LimitedBoxConstrainedBoxAlignPaddingDecoratedBoxTransform 組成。Flutter 的一個很明顯的特徵是,我們可以深入原始碼,去看去檢查原始碼。因此,我們不需要泛化一個 Container 來實現自定義的效果,我們可以把它和另外一些 Widget 進行組合,或者參考 Container 寫一個新的 Widget。

構建 Widget

正如前面提到的,build() 方法返回的內容就是頁面上顯示的內容,返回的元素會形成一顆 Widget 樹,這個樹以更具體的方式來表示 UI 的一部分。比如說,toolbar Widget 的 build 方法 構建了橫向的佈局,包括了 文字、按鈕等等。根據需要,framework 會遞迴的 build 每一個 Widget 直到 Widget 樹可以被更具化的渲染物件完全描述下來。framework 會將渲染物件拼合成一顆渲染樹。

Widget 的 build 方法應該是無副作用的。只要方法被呼叫了,那麼不管舊的 Widget 樹是什麼,一顆新的 Widget 樹都會被建立。framework 做了大量的工作,來決定哪一個 Widget 的 build 方法需要被執行。具體的過程可以參考Inside Flutter topic

在每一個渲染幀,Flutter 僅僅會再次建立 UI 中需要建立的部分,這一部分就是狀態變化的地方,建立的方式是執行 build 方法。所以,build 方法耗時應該非常小。一些比較重的計算應該放在非同步中,將計算的結果作為狀態的一部分,build 方法只是用資料。

雖然這種方式相對有點直白,但是這種自動比較的方式很高效,能夠保正高效能、高保真。而且,build 方法的這種設計可以簡化程式碼,讓開發者聚焦在 Widget 的宣告上,脫離狀態與 UI 複雜的互動。

Widget state

框架裡面有兩個最主要的 Widget 類:StatefulWidget 和 StatelessWidget

許多 Widget 都是無狀態的:它們的屬性不隨著時間改變。這樣的 Widget 是 StatelessWidget 的子類。

但是呢,如果 Widget 的某個特徵需要根據使用者互動或者其他因素髮生改變,那麼這種 Widget 是 StatefulWidget。比如說,如果一個 Widget 有一個計數器,當用戶點選按鈕的時候,計數器需要變化,那麼計數器就是 Widget 的 State。當值改變的時候,Widget 需要被重新構建來更新部分 UI(顯示數字的那部分)。這樣的 Widget 就是 StatefulWidget,因為 Widget 本身是不可變的,所以把狀態儲存在 可變的 State 子類中。StatefulWidget 沒有 build 方法,相反,它的 UI 構建放到了 State 物件中。

只要想要改變 State 物件的狀態,那麼就呼叫 setState() 方法來告訴 framework : 你應該呼叫我的 build 方法來更新 UI 來。

雖然 StatefulWidget 既有 State 物件又有Widget 物件,但是其他 Widget 可以像使用 StatelessWidget 一樣使用 StatefulWidget,擔心狀態丟失等問題。父節點在需要的時候可以隨時建立子元件,不需要保留前一個 state 物件,framework 做了查詢和重用狀態物件的所有工作。

狀態管理

因此,如果保持狀態的 Widget 非常的多,那麼狀態是怎麼管理的呢?是怎麼更好的在應用內傳遞呢?

像其他的類一樣,開發者可以在 Widget 構造方法中初始化它的資料, build() 方法可以確保其用的資料已經初始化了:

dart @override Widget build(BuildContext context) { return ContentWidget(importantState); }

隨著節點樹越來越深,狀態的向上向下查詢就變的十分糟糕了。因此,另一種型別的 Widget —— InheritedWidget 就應運而生了。這種型別的 Widget 提供了一個很容易的方式來獲取祖先節點的資料。可以使用 InheritedWidget 來建立一個 StatefulWidget 祖先節點,就像下面一樣:

image.png

Whenever one of the ExamWidget or ExamWidget objects needs data from StudentState, it can now access it with a command such as: 只要 ExamWidget 或者 ExamWidget 需要 StudentState 的資料,那麼可以使用下面的方式:

dart final studentState = StudentState.of(context);

of(context) 從 context 開始向上查詢,找到最近的指定型別的祖先節點。這裡的型別是StudentStateInheritedWidget 也提供了一個 updateShouldNotify() 方法,這個方法決定了當狀態改變的時候,是否來觸發使用資料的子節點的更新重建。

Flutter 本身就廣泛的使用 InheritedWidget 來共享狀態,比如我們熟知的主題。MaterialAppbuild() 方法中插入了一個 theme 節點,併為 theme 填充了資料,這樣 比theme 節點更深的節點就可以通過 .of() 來找到 theme 節點,並使用資料。比如:

dart Container( color: Theme.of(context).secondaryHeaderColor, child: Text( 'Text with a background color', style: Theme.of(context).textTheme.headline6, ), ); Navigator 也用了這種方式,我們經常使用 Navigator 的 of 方法來 push 或者 pop 路由。MediaQuery 也用這種方式讓開發者可以很快捷的獲取螢幕相關資訊,尺寸、方向、密度、模式等等。

隨著應用的增長,更加先進高階的狀態管理方案更加符合生產環境的開發,可以減少 StatefulWidget 的使用。許多 Flutter 應用使用 provider 這樣的開源庫。前面提到 Flutter 的分層結構可以無限擴充套件,flutter_hooks 這個第三方庫提供了另外一種將狀態轉為 UI 的方式。

渲染與佈局

這一節主要描述渲染管線,渲染管線包括了幾個重要的步驟,將 Widget 真正的轉化為實際的繪製畫素。

Flutter 渲染模型

你可能會好奇:既然 Flutter 是一個跨平臺的框架,它是怎麼做到和單平臺框架相當的效能效果呢?

我們先想一下傳統的 Android app 是怎麼執行的。當需要繪製的時候,開發者需要首先呼叫 Android 框架的 Java 程式碼。Android 系統提供的元件負責在 Canvas 物件中繪製,Android 使用 Skia 進行渲染。Skia 是 C/C++ 開發的圖形庫,會呼叫 CPU 或者 GPU 完成裝置螢幕的繪製。

跨平臺框架通常的做法是:在原生的 Android and iOS UI 層上建立一個抽象層,來嘗試磨平每個平臺的差異性。應用的程式碼一般是解釋語言——JavaScript,必須和Java/Objective-C反覆的互動來顯示 UI。這些都增加了高額的負擔,尤其是 UI 層和邏輯層有大量互動的時候。

Flutter 另闢蹊徑,Flutter 最小化了這些抽象,避開系統提供的 UI,它自己有豐富的 Widget 庫。繪製 Flutter 的 Dart 程式碼最終轉為 native 程式碼,而這些 native 程式碼會使用 Skia 進行渲染。 Flutter 把 Skia 作為 引擎的一部分,這樣開發者可以始終讓應用保持到最新版本,而 Android 裝置不需要更新。IOS等其他的裝置也是相同的道理。

從使用者輸入到 GPU

Flutter 渲染管線的總原則是:簡單就是快,Flutter 有一個簡單明瞭的資料傳輸管道,沿著這個通道使用者的輸入流到了系統。正如下面所示:

image.png

下面我們來看更多的細節。

Build: 從 Widget 到 Element

Consider this code fragment that demonstrates a widget hierarchy: 思考一下下面的 Widget 體系程式碼片段:

dart Container( color: Colors.blue, child: Row( children: [ Image.network('https://www.example.com/1.png'), const Text('A'), ], ), );

當 Flutter 需要渲染這個片段的時候,會呼叫 build 方法,返回了反應當前程式狀態的 Widget 樹,然後去渲染 UI。在這個過程中,build() 方法可能會構造新的 Widget。比如,前面的程式碼中, Container 有 color 和 child 屬性。但是在 Container 的原始碼中,如果 color 不是null,那麼會插入一個代表顏色的 ColoredBox 元件:

dart if (color != null) current = ColoredBox(color: color!, child: current);

同樣地,Image 和 Text 元件也插入了 RawImage 和 RichText 元件在 build 過程中。所以最終的 Widget 樹可能會比程式碼更深,比如:

image.png

這就解釋了為啥我們在 Flutter inspector 看到的節點要遠遠深於我們的原始程式碼。

在 build 階段,Flutter 會將 widget 樹 轉為 element 樹,每一個 Element 都對應一個 Widget。每一個 Element 表示一個指定位置的特定 Widget 例項。有兩個不同型別的 Element:

  • ComponentElement, 承載其他 Element 的 Element
  • RenderObjectElement, 參與佈局和繪製階段的 Element

image.png

RenderObjectElement 是它的 Widget 和 背後的 RenderObject 的中介,這個後面再說。

Widget 的 Element 可以通過 BuildContext 來引用到,同時 BuildContext 也表示樹的位置資訊。比如 Theme.of(context) 的引數就是 BuildContext,並且 BuildContext 是 build 方法的引數。

因為 Widget 是不變的,所以 Widget 樹的任意改變都會導致一組新的 Widget 要被建立,即使是像 Text('A') 到 Text('B') 這樣的變化。但是,Widget 的重新構建,並不意味著背後的 Element、RenderObject 也要重新構建。Element 樹是持久化的在幀與幀之間,Flutter 的高效能很大一原因就是這個持久化的設計。Flutter 會快取 Element等背後的物件,所以完全捨棄舊的Widget 也沒啥問題。通過只遍歷已經修改的 Widget,Flutter可以僅僅重建需要重建的 Element 樹。

佈局和渲染

僅僅繪製一個 Widget 的應用幾乎是不存在的。因此,框架需要高效的佈局 Widget 層次樹,並且在渲染在螢幕上之前,也要高效的計算 Element 的尺寸,標定 Element 的位置。

渲染物件的基類是 RenderObject,這個類定義了佈局和繪製的通用抽象模型,像多維、極座標這樣的需要自定義渲染模型。每個  RenderObject 知道它的父節點是誰,但是子節點的資訊知道的很少,僅僅知道怎麼去 visit 子節點和子節點佈局約束。但是對於抽象來說這就夠了,RenderObject 可以處理各種用例。

在 build 階段,Flutter 會建立或者更新 element 樹上每一個 RenderObjectElement 背後的 RenderObject 物件。 RenderObject 是原始的抽象類:RenderParagraph 渲染文字,RenderImage 渲染影象,RenderTransform 會在子節點繪製之前應用位置等資訊。

image.png

大多數 Flutter Widget 背後的渲染物件是 RenderBox 的子類,RenderBox 將模型定義為盒子模型——固定大小的二維笛卡爾座標。RenderBox 提供基本的 盒子約束,每一個 Widget 都放在由最大最小寬度、最大最小高度限制的盒子內。

為了執行佈局過程,Flutter 向下👇傳遞約束。向上👆傳遞尺寸。父節點設定位置

image.png

佈局的遍歷完成之後,每個物件都有了符合父節點約束的尺寸,就會呼叫 paint() 方法進行繪製。

盒子約束模型非常棒,佈局的過程時間複雜度僅是O(n)的:

  • 父節點可以將最大值和最小值設定為相同的,這樣子節點的大小就是固定的了。比如說,最根部的節點就強制其子節點為螢幕大小。(子節點可以選擇如何使用這一部分空間,比如,子節點可以在空間內居中擺放)

  • 父節點可以讓設定子節點的寬度,但是讓高度靈活可變。或者設定高度,讓寬度靈活可變。比如文字元件,文字元件的寬度是靈活可變的,高度是固定的。

除了上述描述:子節點要根據可用空間的多少來展示自己的顯示也是可行的。使用 LayoutBuilder 可以達到這樣的效果,子節點檢查父節點傳進來的約束,然後使用約束的資訊來展示自己的內容,比如:

dart Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth < 600) { return const OneColumnLayout(); } else { return const TwoColumnLayout(); } }, ); }

佈局和約束更加詳細的內容可以看這一篇文章👉深入理解Flutter佈局約束

所有 RenderObject 的根節點是 RenderView,它就代表一顆渲染樹。當平臺決定繪製新的一幀時,就會呼叫 RenderViewcompositeFrame() 方法,方法內部建立了 SceneBuilder 去更新 scene。當 scene 準備完成之後,RenderView 物件會將合成的 scene 傳遞給 dart:ui 內的 Window.render() 方法,然後 GPU 就會渲染合成資訊。

合成和光柵化階段的更多細節可以參考 👉Flutter 渲染管線

Platform embedding

正如我們所示,Flutter 不像 React Native,把自己的元件轉為 OS 的元件,讓 OS 去渲染,它 是自己完成 build、佈局、合成、繪製。獲取紋理和 App 生命週期的機制也會因為平臺的原因有所不同,比如 Android 的紋理和 IOS 的紋理在實現上就有所不同。Flutter 引擎是平臺無關的,表現出來是 👉應用二進位制介面,提供了一種平臺嵌入器,可以安裝和使用 Flutter。

平臺嵌入器是一個原生系統的應用程式,承載著 Flutter 的所有的內容,並且充當了原生作業系統與 Flutter 之間的粘合劑。當我們開啟 Flutter 應用的時候,嵌入器提供了一個入口:初始化 Flutter 引擎,獲得 UI 執行緒和 光柵,建立 Flutter 紋理。嵌入器也負責:應用的生命週期、手勢輸入、視窗尺寸、執行緒管理和平臺訊息。Flutter 包含了 Android、iOS、Windows、macOS、Linux。開發者也可以自定義平臺嵌入器,有兩個比較不錯的案例 👉 案例1 和 👉 案例2——Raspberry Pi

每一個平臺尤其本身的API和約束。一些簡明的平臺原則如下:

  • 在 iOS 和 macOS,Flutter 是作為 UIViewController 或者 NSViewController 而被載入進嵌入器的。平臺嵌入器建立了 FlutterEngine,而引擎可以當作 Dart VM 和 Flutter 執行時的宿主。FlutterViewControllerFlutterEngine 相繫結,將 UIKit 或者 Cocoa 輸入事件傳遞給 Flutter,並且使用 Metal 或者 OpenGL 來渲染幀。

  • 在 Android 上,Flutter 預設載入到 Activity 中,檢視就是 FlutterViewFlutterView 可以渲染 Flutter 的內容(ui 或者 紋理,取決於合成資訊和 z 軸順序),

  • 在 Windows 上,Flutter 被裝載在傳統的 Win32 應用中。Flutter 內容使用 ANGLE 渲染, 這個庫可以將 OpenGL API 轉為與之等價的 DirectX 11。目前正在做的事情是,提供一個使用 UWP 應用模型的 Windows 嵌入器,以及使用更加高效直接的方式將 DirectX 12 到 GPU,替換現有的 ANGLE。

整合其他程式碼

Flutter 提供了一些互操作的機制,訪問 Kotlin 或者 Swift 編寫的程式碼,呼叫 基於 C 的本地 API,在 Flutter 中嵌入原生元件,在既有應用中嵌入 Flutter。

Platform channels

對於移動端和桌面 App,通過 platform channel 機制,Flutter 可以讓開發者呼叫自定義程式碼。platform channel 是 Dart 程式碼和 App 宿主平臺程式碼通訊的機制。通過建立一個通用的 channel (指定名字和編解碼器),開發者能夠在 Dart 和平臺之間傳送和接受訊息。資料會被序列化,比如 Dart 的 Map 就是 Kotlin 中的 HashMap,Swift 的 Dictionary

image.png

下面是簡單的事件處理器的程式碼,Android 是 Kotlin,iOS 是 Swift,Dart 呼叫原生的方法,並獲取資料:

dart // Dart side const channel = MethodChannel('foo'); final String greeting = await channel.invokeMethod('bar', 'world'); print(greeting);

dart // Android (Kotlin) val channel = MethodChannel(flutterView, "foo") channel.setMethodCallHandler { call, result -> when (call.method) { "bar" -> result.success("Hello, ${call.arguments}") else -> result.notImplemented() } }

dart // iOS (Swift) let channel = FlutterMethodChannel(name: "foo", binaryMessenger: flutterView) channel.setMethodCallHandler { (call: FlutterMethodCall, result: FlutterResult) -> Void in switch (call.method) { case "bar": result("Hello, (call.arguments as! String)") default: result(FlutterMethodNotImplemented) } }

像這樣的 channel 程式碼,可以在 flutter/plugins 倉庫中找到,裡面也有響應的 macOS 的實現。一些通用的場景大概有幾千個可用外掛,從廣告到相機、藍芽這樣的硬體裝置。

外部方法介面

對於 C基礎的 API(包含 Rust、Go 生產的程式碼),Dart 也提供了直接的呼叫機制,可以使用 dart:ffi 依賴庫來繫結 native 程式碼。Foreign function interface (FFI) 模型 沒有資料資料序列化過程,所以它比上面的 channel 更快。Dart 執行時提供了在堆記憶體上分配記憶體的能力,堆上的記憶體是 Dart 物件記憶體,並且可以呼叫靜態和動態的連結庫。FFI 可用在除 web 之外的所有平臺上,因為 js package 提供了相同的能力。

要使用 FFI 的話,可以建立一個 typedef 為每一個 Dart 的非管理的方法簽名,並且指定 Dart VM 做了對映。下面是一個案例,呼叫 Win32 MessageBox() 的 API:

```dart typedef MessageBoxNative = Int32 Function( IntPtr hWnd, Pointer lpText, Pointer lpCaption, Int32 uType, );

typedef MessageBoxDart = int Function( int hWnd, Pointer lpText, Pointer lpCaption, int uType, );

void exampleFfi() { final user32 = DynamicLibrary.open('user32.dll'); final messageBox = user32.lookupFunction('MessageBoxW');

final result = messageBox( 0, // No owner window 'Test message'.toNativeUtf16(), // Message 'Window caption'.toNativeUtf16(), // Window title 0, // OK button only ); } ```

在 Flutter 應用中渲染原生元件

因為 Flutter 內容是被繪製在紋理上的,並且 元件樹完全是內部的。像 Android view 存在在 Flutter 內部模型中,或者在 Flutter 元件交錯渲染,這些情況我們咩有看到。如果不能支援的話,是有問題的,比如一些原生元件不能用的話,就很麻煩。比如 WebView。

Flutter 解決這種問題是通過平臺檢視 Widget 的方式(AndroidView 和 UiKitView)。這些元件可以嵌入平臺原生元件。Platform Widget 可以和其他的 Flutter 內容一起整合,並且充當著與底層作業系統的中介。比如,在Android上,AndroidView 有三個主要的功能:

  • 複製原生檢視的圖形紋理,並把紋理作為 Flutter 合成渲染的一部分,所以在每一幀的時候都會進行這樣的合成繪製。

  • 響應手勢,並且把手勢轉為等價於 Native 的輸入。

  • 建立一個可訪問性樹的模擬,並且在原生和 Flutter 層之間傳遞和響應命令

顯而易見的,每幀的合成都是相當耗時的,像音影片也非常耗記憶體。所以,這種方法一般會在複雜互動的時候採用,比如 Google Maps 這樣的,Flutter 不太具有生產實踐意義的。

通常,一個 Flutter 應用也是在 build() 方法中例項化這些元件,比如,google_maps_flutter建立了地圖外掛:

dart if (defaultTargetPlatform == TargetPlatform.android) { return AndroidView( viewType: 'plugins.flutter.io/google_maps', onPlatformViewCreated: onPlatformViewCreated, gestureRecognizers: gestureRecognizers, creationParams: creationParams, creationParamsCodec: const StandardMessageCodec(), ); } else if (defaultTargetPlatform == TargetPlatform.iOS) { return UiKitView( viewType: 'plugins.flutter.io/google_maps', onPlatformViewCreated: onPlatformViewCreated, gestureRecognizers: gestureRecognizers, creationParams: creationParams, creationParamsCodec: const StandardMessageCodec(), ); } return Text( '$defaultTargetPlatform is not yet supported by the maps plugin');

AndroidView 或者 UiKitView 使用的我們前面提到的 platform channel 機制來和 原生程式碼互動。

目前,Platform Widget 在桌面平臺上是不可用的,但是這不是架構上的限制,後面可能會新增。

宿主 App 接入 Flutter

和前面Flutter 嵌入 native 相反,這一節介紹 既有應用中嵌入 Flutter。前面我們提到了 Flutter 是被 Android 的 Activity,iOS 的 UIViewController 承載的,Flutter 的內容可以用相同的方式被嵌入。

Flutter module 模版很容易被嵌入,開發者可以使用 Gradle 或者 Xcode 進行原始碼依賴,也可以產物依賴。產物依賴的方式的好處就是專案組的成員不需要每個人都安裝 Flutter 環境。

Flutter 引擎需要一點的時間初始化,因為需要載入 Flutter 依賴庫,初始化 Dart 執行時,建立和執行 Dart 執行緒,繫結渲染 surface 到 UI。為了最小化上面提到的時間,減少呈現 Flutter UI 的延遲,最好的處理方式是在程式初始化的時候,初始化 Flutter 引擎,至少在第一個 第一個Flutter螢幕之前,這樣使用者不就會在顯示 Flutter 第一個頁面的時候,出現短暫的白屏或者黑屏。

關於接入的更多資訊,可以在 👉Load sequence, performance and memory topic找到。

Flutter web support

一些通用的框架概念適用於 Flutter 支援的所有平臺,但是呢,Flutter’s web 有一些值得討論的獨特的點。

自JavaScript語言存在以來,Dart就一直在編譯JavaScript,併為開發和生產目的優化了工具鏈。許多重要的應用程式從Dart編譯到JavaScript,並在今天的生產中執行,包括谷歌Ads的廣告商工具。因為Flutter框架是用Dart編寫的,所以將它編譯成JavaScript相對簡單。

從 Dart 語言面世以來,Dart 就一直在支援編譯成 JavaScript,並且持續的為開發和生產優化工具鏈。許多重要的程式從 Dart 編譯成 JavaScript,並在今天一直在執行,比如 👉advertiser tooling for Google Ads。因為 Flutter 框架是 Dart 開發的,把 Dart 編譯成 JavaScript 相對來說是簡單直接的。

However, the Flutter engine, written in C++, is designed to interface with the underlying operating system rather than a web browser. A different approach is therefore required. On the web, Flutter provides a reimplementation of the engine on top of standard browser APIs. We currently have two options for rendering Flutter content on the web: HTML and WebGL. In HTML mode, Flutter uses HTML, CSS, Canvas, and SVG. To render to WebGL, Flutter uses a version of Skia compiled to WebAssembly called CanvasKit. While HTML mode offers the best code size characteristics, CanvasKit provides the fastest path to the browser’s graphics stack, and offers somewhat higher graphical fidelity with the native mobile targets5. 然而,C++ 開發的 Flutter 引擎是作業系統底層的介面,而不是瀏覽器。因此,需要採取一個不同的方法。在 web 上,Flutter 在標準瀏覽器 API 之上 提供了重新實現。目前,在 Web 上渲染 Flutter 有兩個方案:HTML 和 WebGL。HTML 模式下,Flutter 使用 HTML、 CSS、 Canvas 和 SVG。WebGL 模式下,Flutter 使用 CanvasKit 編譯成 WebAssembly。 HTML 模式的包體積會很小,而 CanvasKit 的渲染會更快、渲染效果更佳高保真。

Web 版本的架構圖是下面的:

Flutter web
architecture

和其他 Flutter 平臺相比,最顯著的區別是:不需要提供一個 Dart 的執行時。相反,Flutter 的framework 被編譯成了 JavaScript。在 Dart 的眾多模式中,比如 JIT、AOT、native、web,語言語義上的差別很小,開發者也不會在開發的過程中體驗到差異。

在開發期間,Flutter web 使用 dartdevc,它支援增量編譯,這就可以 hot restart 了。相反,如果想要建立一個線上正式版本的 web,就會使用 dart2js 編譯器,這是一款高效能的 JavaScript 編譯器,會將 Flutter 核心和框架與應用程式打包為可部署到任何 web 伺服器的小型原始檔。deferred imports 可以將程式碼封裝為一個檔案,或者分割為多個檔案。

展望

如果想要更加深入瞭解 Flutter 內部的機制,那麼可以看 Inside Flutter 文章。