Flutter 架構概觀
本文旨在提供 Flutter 架構的高階概觀,包括形成其設計的核心原則和概念。
Flutter 是一個跨平台的 UI 工具包,旨在讓程式碼能夠在 iOS 和 Android 等作業系統之間重複使用,同時也允許應用程式直接與底層的平台服務介接。其目標是讓開發人員能夠交付在不同平台上感覺自然的、高效能應用程式,在儘可能多地共用程式碼的同時,也接納不同之處。
在開發期間,Flutter 應用程式會在 VM 中執行,提供變更的狀態式熱重載,而無需完全重新編譯。在發布時,Flutter 應用程式會直接編譯為機器碼,無論是 Intel x64 或 ARM 指令,或是如果以 Web 為目標,則會編譯為 JavaScript。該框架是開源的,採用寬鬆的 BSD 授權,並且擁有蓬勃發展的第三方套件生態系統,這些套件補充了核心程式庫的功能。
本概述分為以下幾個部分
- 層級模型:構成 Flutter 的各個部分。
- 反應式使用者介面:Flutter 使用者介面開發的核心概念。
- Widget 簡介:Flutter 使用者介面的基本建構區塊。
- 渲染流程:Flutter 如何將 UI 程式碼轉換為像素。
- 平台嵌入器的概述:讓行動裝置和桌上型作業系統執行 Flutter 應用程式的程式碼。
- 將 Flutter 與其他程式碼整合:關於 Flutter 應用程式可用的不同技術的資訊。
- 對 Web 的支援:關於 Flutter 在瀏覽器環境中的特性的總結性評論。
架構層
#Flutter 設計為可延伸的分層系統。它以一系列獨立的程式庫存在,每個程式庫都依賴於底層。沒有任何一層可以優先存取其下方的一層,並且框架層的每個部分都設計為可選且可替換的。
對於底層作業系統而言,Flutter 應用程式的封裝方式與任何其他原生應用程式相同。平台特定的嵌入器提供一個進入點;與底層作業系統協調,以存取諸如渲染表面、協助工具和輸入等服務;並管理訊息事件迴圈。嵌入器以適合該平台的語言編寫:目前 Android 為 Java 和 C++,iOS 和 macOS 為 Objective-C/Objective-C++,Windows 和 Linux 為 C++。使用嵌入器,Flutter 程式碼可以整合到現有的應用程式中作為模組,或者程式碼可能是整個應用程式的內容。Flutter 包含許多用於常見目標平台的嵌入器,但也存在其他嵌入器。
Flutter 的核心是 Flutter 引擎,它主要以 C++ 編寫,並支援所有 Flutter 應用程式所需的基礎元素。引擎負責在需要繪製新影格時,光柵化複合場景。它提供了 Flutter 核心 API 的底層實作,包括圖形(透過 iOS 上的 Impeller,以及即將在 Android 和 macOS 上推出的版本,以及其他平台上的 Skia)、文字配置、檔案和網路 I/O、協助工具支援、外掛程式架構以及 Dart 執行階段和編譯工具鏈。
引擎透過 dart:ui
暴露給 Flutter 框架,這會將底層 C++ 程式碼包裝在 Dart 類別中。這個程式庫會公開最低層級的基礎元素,例如用於驅動輸入、圖形和文字渲染子系統的類別。
通常,開發人員會透過 Flutter 框架與 Flutter 互動,它提供以 Dart 語言編寫的現代反應式框架。它包含豐富的平台、版面配置和基礎程式庫,由一系列層組成。由下往上,我們有
- 基本的 基礎 類別,以及建構區塊服務,例如 動畫、繪圖 和 手勢,它們提供對底層基礎結構的常用抽象。
- 渲染層提供處理版面配置的抽象。使用這一層,您可以建立可渲染物件的樹狀結構。您可以動態操作這些物件,而樹狀結構會自動更新版面配置以反映您的變更。
- Widget 層是一種組合抽象。渲染層中的每個渲染物件在 Widget 層中都有對應的類別。此外,Widget 層還允許您定義可以重複使用的類別組合。這是在此引入反應式程式設計模型的層。
- Material 和 Cupertino 程式庫提供全面的控制元件集,這些控制元件使用 Widget 層的組合基礎元素來實作 Material 或 iOS 設計語言。
Flutter 框架相對較小;開發人員可能會使用的許多更高階功能都以套件的形式實作,包括諸如 相機 和 webview 等平台外掛程式,以及諸如 字元、http 和 動畫 等與平台無關的功能,它們都建立在核心 Dart 和 Flutter 程式庫之上。其中一些套件來自更廣泛的生態系統,涵蓋諸如 應用程式內付款、Apple 身份驗證 和 動畫 等服務。
本概述的其餘部分會大致沿著層級向下導航,首先從 UI 開發的反應式範例開始。然後,我們描述如何將 Widget 組合在一起,並轉換為可以作為應用程式一部分渲染的物件。我們描述 Flutter 如何在平台層級與其他程式碼互通,然後簡要總結 Flutter 的 Web 支援與其他目標的不同之處。
應用程式剖析
#下圖概述了由 flutter create
產生的常規 Flutter 應用程式的組成部分。它顯示了 Flutter 引擎在此堆疊中的位置,突顯了 API 邊界,並識別了各個部分所在的儲存庫。下面的圖例闡明了一些常用於描述 Flutter 應用程式各部分的術語。
Dart 應用程式
- 將 Widget 組合成所需的使用者介面。
- 實作商業邏輯。
- 由應用程式開發人員擁有。
框架 (原始碼)
- 提供更高層級的 API 來建立高品質的應用程式(例如,Widget、點擊測試、手勢偵測、協助工具、文字輸入)。
- 將應用程式的 Widget 樹狀結構組合成場景。
引擎 (原始碼)
- 負責光柵化複合場景。
- 提供 Flutter 核心 API 的底層實作(例如,圖形、文字配置、Dart 執行階段)。
- 使用 dart:ui API 將其功能暴露給框架。
- 使用引擎的 嵌入器 API 與特定平台整合。
嵌入器 (原始碼)
- 與底層作業系統協調,以存取諸如渲染表面、協助工具和輸入等服務。
- 管理事件迴圈。
- 公開 平台特定 API 以將嵌入器整合到應用程式中。
執行器
- 將嵌入器的平台特定 API 公開的部分組合成可在目標平台上執行的應用程式封裝。
- 由
flutter create
產生的應用程式範本的一部分,由應用程式開發人員擁有。
反應式使用者介面
#從表面上看,Flutter 是一個反應式、宣告式 UI 框架,其中開發人員提供從應用程式狀態到介面狀態的對應,而框架負責在應用程式狀態變更時更新介面。這個模型受到 Facebook 為其自身的 React 框架所做的研究的啟發,其中包括對許多傳統設計原則的重新思考。
在大多數傳統 UI 框架中,使用者介面的初始狀態會描述一次,然後在執行階段由使用者程式碼單獨更新,以回應事件。此方法的一個挑戰是,隨著應用程式複雜性的增加,開發人員需要了解狀態變更如何在整個 UI 中層疊。例如,請考慮以下 UI
狀態可以在許多地方被更改:顏色方塊、色調滑桿、單選按鈕。當使用者與 UI 互動時,這些變更必須反映在所有其他地方。更糟的是,除非特別小心,否則使用者介面的某個小變更可能會對看似不相關的程式碼片段產生連鎖效應。
其中一個解決方案是類似 MVC 的方法,您透過控制器將資料變更推送到模型,然後模型再透過控制器將新狀態推送到視圖。然而,這也存在問題,因為建立和更新 UI 元素是兩個獨立的步驟,很容易失去同步。
Flutter 和其他響應式框架採用另一種方法來解決這個問題,它們明確地將使用者介面與其底層狀態分離。透過 React 風格的 API,您只需建立 UI 描述,框架會負責使用該配置來建立和/或更新使用者介面(如適當)。
在 Flutter 中,Widget(類似於 React 中的元件)由不可變的類別表示,這些類別用於配置物件樹狀結構。這些 Widget 用於管理單獨的佈局物件樹狀結構,然後再用於管理單獨的合成物件樹狀結構。Flutter 的核心是一系列的機制,用於有效地遍歷樹狀結構的修改部分,將物件樹狀結構轉換為更低層級的物件樹狀結構,並在這些樹狀結構中傳播變更。
Widget 透過覆寫 build()
方法來宣告其使用者介面,該方法是一個將狀態轉換為 UI 的函數。
UI = f(state)
build()
方法在設計上執行速度很快,應該沒有副作用,允許框架在需要時(可能高達每渲染幀一次)呼叫它。
這種方法依賴於語言運行時的某些特性(特別是快速物件實例化和刪除)。幸運的是,Dart 特別適合這項任務。
Widget
#如前所述,Flutter 強調 Widget 作為組合的單位。Widget 是 Flutter 應用程式使用者介面的建構模塊,每個 Widget 都是使用者介面一部分的不可變宣告。
Widget 基於組合形成層次結構。每個 Widget 嵌套在其父元件內部,並可以接收來自父元件的上下文。這個結構一路延伸到根 Widget(託管 Flutter 應用程式的容器,通常是 MaterialApp
或 CupertinoApp
),如下面的簡單範例所示
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('My Home Page'),
),
body: Center(
child: Builder(
builder: (context) {
return Column(
children: [
const Text('Hello World'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
print('Click!');
},
child: const Text('A button'),
),
],
);
},
),
),
),
);
}
}
在上面的程式碼中,所有實例化的類別都是 Widget。
應用程式透過告訴框架將層次結構中的一個 Widget 替換為另一個 Widget 來響應事件(例如使用者互動)來更新其使用者介面。然後,框架會比較新的和舊的 Widget,並有效地更新使用者介面。
Flutter 有自己的每個 UI 控制項的實作,而不是延遲到系統提供的控制項:例如,有一個純 Dart 實作的 iOS 切換控制項和 Android 等效項的實作。
這種方法提供了幾個好處:
- 提供無限的擴展性。想要一個 Switch 控制項變體的開發人員可以以任何任意方式創建一個,並且不受作業系統提供的擴展點限制。
- 允許 Flutter 一次合成整個場景,而無需在 Flutter 程式碼和平台程式碼之間來回切換,從而避免了嚴重的效能瓶頸。
- 將應用程式行為與任何作業系統依賴關係分離。應用程式在所有版本的作業系統上看起來和感覺都一樣,即使作業系統更改了其控制項的實作。
組合
#Widget 通常由許多其他小的、單一用途的 Widget 組成,它們組合起來產生強大的效果。
在可能的情況下,設計概念的數量保持在最低限度,同時允許總詞彙量很大。例如,在 Widget 層中,Flutter 使用相同的核心概念 (Widget
) 來表示繪製到螢幕、佈局(定位和調整大小)、使用者互動、狀態管理、主題、動畫和導航。在動畫層中,一對概念 Animation
和 Tween
涵蓋了大部分設計空間。在渲染層中,RenderObject
用於描述佈局、繪製、點擊測試和輔助功能。在每種情況下,相應的詞彙都很大:有數百個 Widget 和渲染物件,以及數十種動畫和 Tween 類型。
類別層次結構經過精心設計,具有淺而寬的特性,以最大化可能的組合數量,重點是小的、可組合的 Widget,每個 Widget 都做得很好。核心功能是抽象的,即使是基本的特性(例如內邊距和對齊)也是作為單獨的元件實現,而不是構建到核心中。(這也與更傳統的 API 形成對比,在這些 API 中,內邊距等功能是構建到每個佈局元件的通用核心中的。)因此,例如,要使 Widget 居中,您不需要調整一個假想的 Align
屬性,而是將其包裝在 Center
Widget 中。
有用於內邊距、對齊、行、列和網格的 Widget。這些佈局 Widget 本身沒有視覺表示。相反,它們的唯一目的是控制另一個 Widget 的佈局的某些方面。Flutter 還包括利用這種組合方法的實用 Widget。
例如,Container
是一個常用的 Widget,它由幾個負責佈局、繪製、定位和調整大小的 Widget 組成。具體來說,Container 由 LimitedBox
、ConstrainedBox
、Align
、Padding
、DecoratedBox
和 Transform
Widget 組成,您可以透過閱讀其原始碼來查看。Flutter 的一個決定性特徵是,您可以深入研究任何 Widget 的原始碼並檢查它。因此,您可以透過新穎的方式組合它和其他 Widget,或者只是使用 Container
作為靈感創建一個新的 Widget,而不是對 Container
進行子類化來產生自訂的效果。
建立 Widget
#如前所述,您透過覆寫 build()
函數來確定 Widget 的視覺表示,以傳回新的元素樹狀結構。這個樹狀結構以更具體的方式表示 Widget 在使用者介面中的一部分。例如,工具列 Widget 可能有一個 build 函數,該函數傳回一些 文字和 各種 按鈕的 水平佈局。根據需要,框架會遞迴地要求每個 Widget 建構,直到樹狀結構完全由 具體的、可渲染的物件描述。然後,框架將可渲染的物件拼接成可渲染的物件樹狀結構。
Widget 的 build 函數應該沒有副作用。每當要求函數建構時,Widget 都應該傳回一個新的 Widget 樹狀結構[1],無論 Widget 先前傳回什麼。框架會完成繁重的工作,以根據渲染物件樹狀結構確定需要呼叫哪些建構方法(稍後會更詳細地描述)。有關此過程的更多資訊,請參閱 Inside Flutter 主題。
在每個渲染幀上,Flutter 可以透過呼叫該 Widget 的 build()
方法來重新建立 UI 中狀態已變更的部分。因此,重要的是 build 方法應該快速傳回,而繁重的計算工作應該以非同步方式完成,然後作為狀態的一部分儲存,以供 build 方法使用。
雖然方法相對簡單,但這種自動比較非常有效,可實現高效能的互動式應用程式。而且,build 函數的設計簡化了您的程式碼,方法是專注於宣告 Widget 是由什麼構成的,而不是從一個狀態到另一個狀態更新使用者介面的複雜性。
Widget 狀態
#框架引入了兩大類 Widget:有狀態和無狀態 Widget。
許多 Widget 沒有可變狀態:它們沒有任何隨著時間而改變的屬性(例如,圖示或標籤)。這些 Widget 是 StatelessWidget
的子類別。
但是,如果 Widget 的獨特特性需要根據使用者互動或其他因素而改變,則該 Widget 是有狀態的。例如,如果一個 Widget 有一個計數器,每當使用者點擊按鈕時,計數器就會遞增,則計數器的值就是該 Widget 的狀態。當該值變更時,需要重新建構 Widget 以更新其在 UI 中的部分。這些 Widget 是 StatefulWidget
的子類別,並且(由於 Widget 本身是不可變的)它們將可變狀態儲存在一個單獨的類別中,該類別是 State
的子類別。StatefulWidget
沒有建構方法;相反,它們的使用者介面是透過它們的 State
物件建構的。
每當您變更 State
物件時(例如,透過遞增計數器),您必須呼叫 setState()
來通知框架透過再次呼叫 State
的建構方法來更新使用者介面。
擁有單獨的狀態和 Widget 物件,讓其他 Widget 可以以完全相同的方式處理無狀態和有狀態 Widget,而不用擔心失去狀態。父元件可以隨時建立子元件的新實例,而不會遺失子元件的持久狀態,而不是需要保留子元件以保留其狀態。框架會完成在適當時尋找和重複使用現有狀態物件的所有工作。
狀態管理
#因此,如果許多 Widget 可以包含狀態,那麼狀態是如何管理並在系統中傳遞的呢?
與任何其他類別一樣,您可以在 Widget 中使用建構函式來初始化其資料,因此 build()
方法可以確保任何子 Widget 都會使用其所需的資料進行實例化。
@override
Widget build(BuildContext context) {
return ContentWidget(importantState);
}
其中 importantState
是包含對 Widget
很重要的狀態的類別的佔位符。
但是,隨著 Widget 樹狀結構變得更深,在樹狀結構層次結構中上下傳遞狀態資訊變得麻煩。因此,第三種類型的 Widget InheritedWidget
提供了一種簡單的方法來從共用祖先獲取資料。您可以使用 InheritedWidget
建立一個狀態 Widget,該 Widget 會在 Widget 樹狀結構中包裝一個常見的祖先,如本範例所示
每當其中一個 ExamWidget
或 GradeWidget
物件需要來自 StudentState
的資料時,它現在可以使用如下命令來存取它
final studentState = StudentState.of(context);
of(context)
呼叫採用建構上下文(當前 Widget 位置的控制代碼),並傳回樹狀結構中 最接近的、符合 StudentState
類型的祖先。InheritedWidget
還提供了一個 updateShouldNotify()
方法,Flutter 會呼叫此方法來確定狀態變更是否應觸發使用它的子 Widget 的重新建構。
Flutter 本身大量使用 InheritedWidget
作為框架的一部分,用於共享狀態,例如應用程式的視覺主題,其中包含 顏色和類型樣式等屬性,這些屬性在整個應用程式中普遍存在。當 MaterialApp
的 build()
方法建構時,會在樹狀結構中插入一個主題,然後在層次結構的更深處,小工具可以使用 .of()
方法來查找相關的主題數據。
例如:
Container(
color: Theme.of(context).secondaryHeaderColor,
child: Text(
'Text with a background color',
style: Theme.of(context).textTheme.titleLarge,
),
);
隨著應用程式的成長,更先進的狀態管理方法變得更具吸引力,這些方法可以減少建立和使用有狀態小工具的繁瑣步驟。許多 Flutter 應用程式使用像 provider 這樣的實用套件,它提供了一個 InheritedWidget
的封裝器。Flutter 的分層架構也支援其他方法來實現狀態到 UI 的轉換,例如 flutter_hooks 套件。
渲染與版面配置
#本節描述渲染管線,這是 Flutter 將小工具層次結構轉換為繪製到螢幕上的實際像素所採取的一系列步驟。
Flutter 的渲染模型
#您可能會想:如果 Flutter 是一個跨平台框架,那麼它如何提供與單一平台框架相當的效能?
首先思考傳統 Android 應用程式的工作方式會很有幫助。在繪製時,您首先呼叫 Android 框架的 Java 程式碼。Android 系統函式庫提供了負責將自己繪製到 Canvas
物件的組件,然後 Android 可以使用 Skia 進行渲染,Skia 是一個用 C/C++ 編寫的圖形引擎,它呼叫 CPU 或 GPU 來完成裝置上的繪製。
跨平台框架通常透過在底層的原生 Android 和 iOS UI 函式庫上建立一個抽象層來工作,試圖消除每個平台表示的不一致性。應用程式程式碼通常是用 JavaScript 等直譯語言編寫的,它必須反過來與基於 Java 的 Android 或基於 Objective-C 的 iOS 系統函式庫互動才能顯示 UI。所有這些都會增加額外的開銷,尤其是在 UI 和應用程式邏輯之間存在大量互動的情況下。
相比之下,Flutter 最大限度地減少了這些抽象,放棄了系統 UI 小工具函式庫,而選擇了自己的小工具集。繪製 Flutter 視覺效果的 Dart 程式碼會被編譯成原生程式碼,該程式碼使用 Skia(或未來使用 Impeller)進行渲染。Flutter 還將其自己的 Skia 副本嵌入為引擎的一部分,允許開發人員升級他們的應用程式以保持最新的效能改進,即使手機尚未更新為新的 Android 版本也是如此。對於其他原生平台上的 Flutter,例如 Windows 或 macOS,情況也是如此。
從使用者輸入到 GPU
#Flutter 應用於其渲染管線的首要原則是 簡單即快速。Flutter 有一個簡單的管線來處理資料如何流向系統,如下面的循序圖所示
讓我們更詳細地了解一下這些階段。
建置:從 Widget 到 Element
#請考慮以下程式碼片段,它示範了一個小工具層次結構
Container(
color: Colors.blue,
child: Row(
children: [
Image.network('https://www.example.com/1.png'),
const Text('A'),
],
),
);
當 Flutter 需要渲染此片段時,它會呼叫 build()
方法,該方法會傳回一個小工具的子樹,該子樹根據目前的應用程式狀態渲染 UI。在此過程中,build()
方法可以根據需要引入新的小工具。例如,在前面的程式碼片段中,Container
具有 color
和 child
屬性。從 Container
的原始碼 中,您可以看到如果顏色不為 null,它會插入一個代表顏色的 ColoredBox
if (color != null)
current = ColoredBox(color: color!, child: current);
相應地,Image
和 Text
小工具可能會在建構過程中插入子小工具,例如 RawImage
和 RichText
。因此,最終的小工具層次結構可能會比程式碼表示的更深,如本例所示[2]
這解釋了為什麼當您透過除錯工具(例如 Flutter inspector,這是 Flutter/Dart DevTools 的一部分)檢查樹狀結構時,您可能會看到一個比原始程式碼中深得多的結構。
在建構階段,Flutter 會將程式碼中表示的小工具轉換為相應的元素樹,每個小工具都有一個元素。每個元素都代表樹狀結構中特定位置的小工具的特定實例。元素有兩種基本類型
ComponentElement
,其他元素的容器。RenderObjectElement
,一個參與版面配置或繪製階段的元素。
RenderObjectElement
是它們的小工具類比和底層的 RenderObject
之間的媒介,我們稍後將會介紹。
任何小工具的元素都可以透過其 BuildContext
來引用,它是小工具在樹狀結構中位置的處理器。這是函數呼叫(例如 Theme.of(context)
)中的 context
,並且作為參數提供給 build()
方法。
由於小工具是不可變的,包括節點之間的父/子關係,因此對小工具樹狀結構的任何變更(例如將前面的範例中的 Text('A')
變更為 Text('B')
)都會導致傳回一組新的小工具物件。但這並不意味著必須重建底層表示。元素樹在每一幀之間都是持久的,因此扮演著關鍵的效能角色,允許 Flutter 表現得好像小工具層次結構是完全可拋棄的,同時快取其底層表示。透過僅遍歷已變更的小工具,Flutter 可以僅重建需要重新配置的元素樹部分。
版面配置與渲染
#很少有應用程式只繪製單一小工具。因此,任何 UI 框架的一個重要部分是能夠有效率地配置小工具層次結構,在將每個元素渲染到螢幕上之前確定它們的大小和位置。
渲染樹中每個節點的基底類別是 RenderObject
,它為版面配置和繪製定義了一個抽象模型。這是非常通用的:它不承諾固定數量的維度,甚至不承諾笛卡爾坐標系(這個極坐標系範例證明了這一點)。每個 RenderObject
都知道其父項,但除了如何訪問它們及其約束之外,對其子項知之甚少。這為 RenderObject
提供了足夠的抽象,使其能夠處理各種使用案例。
在建構階段,Flutter 為元素樹中的每個 RenderObjectElement
建立或更新一個繼承自 RenderObject
的物件。RenderObject
是基本類型:RenderParagraph
渲染文字,RenderImage
渲染影像,而 RenderTransform
在繪製其子項之前應用轉換。
大多數 Flutter 小工具由繼承自 RenderBox
子類別的物件渲染,該子類別表示 2D 笛卡爾空間中固定大小的 RenderObject
。RenderBox
提供了盒約束模型的基礎,為每個要渲染的小工具建立最小和最大寬度和高度。
為了執行版面配置,Flutter 以深度優先遍歷方式遍歷渲染樹,並將大小約束從父項傳遞給子項。在確定其大小時,子項必須遵守其父項給予它的約束。子項會在其父項建立的約束範圍內,將大小傳遞給其父項物件。
在單次遍歷樹狀結構結束時,每個物件在其父項的約束內都有一個定義的大小,並準備好透過呼叫 paint()
方法來繪製。
盒約束模型是按 O(n) 時間配置物件的非常強大的方法
- 父項可以透過將最大和最小約束設定為相同的值來指定子物件的大小。例如,手機應用程式中最頂層的渲染物件會將其子項限制為螢幕的大小。(子項可以選擇如何使用該空間。例如,它們可能只是將它們想要渲染的內容置於指定的約束內。)
- 父項可以指定子項的寬度,但給予子項高度的彈性(或指定高度,但提供寬度的彈性)。一個實際的範例是流動文字,它可能必須符合水平約束,但會根據文字量垂直變化。
即使子物件需要知道它有多少可用空間來決定如何渲染其內容,此模型也有效。透過使用 LayoutBuilder
小工具,子物件可以檢查傳遞下來的約束,並使用這些約束來決定如何使用它們,例如
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 600) {
return const OneColumnLayout();
} else {
return const TwoColumnLayout();
}
},
);
}
有關約束和版面配置系統的更多資訊,以及工作範例,可以在了解約束主題中找到。
所有 RenderObject
的根是 RenderView
,它表示渲染樹的總輸出。當平台要求渲染新影格時(例如,由於 vsync 或由於紋理解壓縮/上傳完成),會呼叫 compositeFrame()
方法,該方法是渲染樹根部的 RenderView
物件的一部分。這會建立一個 SceneBuilder
來觸發場景的更新。當場景完成時,RenderView
物件會將組合好的場景傳遞給 dart:ui
中的 Window.render()
方法,該方法會將控制權傳遞給 GPU 以進行渲染。
管線的合成和點陣化階段的更多詳細資訊超出了這篇高層次文章的範圍,但更多資訊可以在 這個關於 Flutter 渲染管線的演講中找到。
平台嵌入
#正如我們所見,Flutter 使用者介面不是被翻譯成等效的作業系統小工具,而是由 Flutter 本身建構、配置、合成和繪製。取得紋理並參與底層作業系統的應用程式生命週期的機制不可避免地會因該平台的獨特問題而有所不同。引擎與平台無關,提供一個 穩定的 ABI(應用程式二進位介面),為平台嵌入器提供了一種設定和使用 Flutter 的方法。
平台嵌入器是託管所有 Flutter 內容的原生作業系統應用程式,並且充當主機作業系統和 Flutter 之間的橋樑。當您啟動 Flutter 應用程式時,嵌入器會提供進入點、初始化 Flutter 引擎、取得 UI 和點陣化執行緒,並建立 Flutter 可以寫入的紋理。嵌入器還負責應用程式生命週期,包括輸入手勢(例如滑鼠、鍵盤、觸控)、視窗大小調整、執行緒管理和平台訊息。Flutter 包括 Android、iOS、Windows、macOS 和 Linux 的平台嵌入器;您也可以建立自訂平台嵌入器,如 這個支援透過 VNC 樣式畫面緩衝區遠端控制 Flutter 工作階段的工作範例或 這個適用於 Raspberry Pi 的工作範例。
每個平台都有自己的一組 API 和約束。一些簡短的平台特定注意事項
- 在 iOS 和 macOS 上,Flutter 分別以
UIViewController
或NSViewController
的形式載入到嵌入器中。平台嵌入器會建立一個FlutterEngine
,作為 Dart VM 和您的 Flutter 執行時期的主機,以及一個FlutterViewController
,它會連接到FlutterEngine
,將 UIKit 或 Cocoa 輸入事件傳遞到 Flutter,並使用 Metal 或 OpenGL 顯示由FlutterEngine
渲染的幀。 - 在 Android 上,預設情況下,Flutter 會以
Activity
的形式載入到嵌入器中。視圖由FlutterView
控制,它會根據 Flutter 內容的組成和 Z 軸排序需求,以視圖或紋理的形式渲染 Flutter 內容。 - 在 Windows 上,Flutter 託管在傳統的 Win32 應用程式中,並使用 ANGLE 渲染內容。ANGLE 是一個將 OpenGL API 呼叫轉換為 DirectX 11 等效項的函式庫。
與其他程式碼整合
#Flutter 提供了多種互通性機制,無論您是存取以 Kotlin 或 Swift 等語言編寫的程式碼或 API、呼叫基於 C 的原生 API、在 Flutter 應用程式中嵌入原生控制項,還是將 Flutter 嵌入現有的應用程式中。
平台通道
#對於行動和桌面應用程式,Flutter 允許您透過平台通道呼叫自訂程式碼。平台通道是一種在您的 Dart 程式碼和主機應用程式的平台特定程式碼之間進行通訊的機制。透過建立一個通用的通道(封裝名稱和編碼解碼器),您可以在 Dart 和以 Kotlin 或 Swift 等語言編寫的平台元件之間傳送和接收訊息。資料會從 Dart 類型(如 Map
)序列化為標準格式,然後反序列化為 Kotlin(例如 HashMap
)或 Swift(例如 Dictionary
)中的等效表示法。
以下是一個簡短的平台通道範例,說明 Dart 如何呼叫 Kotlin (Android) 或 Swift (iOS) 中的接收事件處理程式。
// Dart side
const channel = MethodChannel('foo');
final greeting = await channel.invokeMethod('bar', 'world') as String;
print(greeting);
// Android (Kotlin)
val channel = MethodChannel(flutterView, "foo")
channel.setMethodCallHandler { call, result ->
when (call.method) {
"bar" -> result.success("Hello, ${call.arguments}")
else -> result.notImplemented()
}
}
// 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)
}
}
更多關於使用平台通道的範例,包括桌面平台的範例,可以在 flutter/packages 儲存庫中找到。還有 數千個已可用的外掛程式 適用於 Flutter,涵蓋了許多常見的應用情境,從 Firebase 到廣告,再到攝影機和藍牙等裝置硬體。
外部函式介面
#對於基於 C 的 API,包括那些可以為以 Rust 或 Go 等現代語言編寫的程式碼產生的 API,Dart 提供了一種直接的機制,可以使用 dart:ffi
函式庫來繫結到原生程式碼。外部函式介面 (FFI) 模型可能比平台通道快得多,因為傳遞資料時不需要序列化。相反地,Dart 執行時期提供了在由 Dart 物件支援的堆積上配置記憶體的能力,並呼叫靜態或動態連結的函式庫。FFI 適用於除了網頁以外的所有平台,在網頁上,JS 互通函式庫 和 package:web
提供了類似的功能。
若要使用 FFI,您需要為每個 Dart 和非受管理的方法簽名建立 typedef
,並指示 Dart VM 在它們之間進行對應。舉例來說,以下是一段程式碼片段,用於呼叫傳統的 Win32 MessageBox()
API。
import 'dart:ffi';
import 'package:ffi/ffi.dart'; // contains .toNativeUtf16() extension method
typedef MessageBoxNative = Int32 Function(
IntPtr hWnd,
Pointer<Utf16> lpText,
Pointer<Utf16> lpCaption,
Int32 uType,
);
typedef MessageBoxDart = int Function(
int hWnd,
Pointer<Utf16> lpText,
Pointer<Utf16> lpCaption,
int uType,
);
void exampleFfi() {
final user32 = DynamicLibrary.open('user32.dll');
final messageBox =
user32.lookupFunction<MessageBoxNative, MessageBoxDart>('MessageBoxW');
final result = messageBox(
0, // No owner window
'Test message'.toNativeUtf16(), // Message
'Window caption'.toNativeUtf16(), // Window title
0, // OK button only
);
}
在 Flutter 應用程式中渲染原生控制元件
#因為 Flutter 內容會繪製到紋理,且其 Widget 樹狀結構完全是內部的,所以像 Android 視圖之類的物件在 Flutter 的內部模型中沒有位置,或者在 Flutter Widget 中沒有交錯渲染的位置。對於想要在其 Flutter 應用程式中包含現有平台元件(例如瀏覽器控制項)的開發人員來說,這是一個問題。
Flutter 透過引入平台視圖 Widget(AndroidView
和 UiKitView
)來解決這個問題,這些 Widget 可讓您在每個平台上嵌入這種類型的內容。平台視圖可以與其他 Flutter 內容整合[3]。每個 Widget 都充當底層作業系統的中介。例如,在 Android 上,AndroidView
具有三個主要功能:
- 複製原生視圖渲染的圖形紋理,並將其呈現給 Flutter,以便在每次繪製影格時將其組合成 Flutter 渲染的表面的一部分。
- 回應點擊測試和輸入手勢,並將其轉換為等效的原生輸入。
- 建立輔助功能樹狀結構的類比,並在原生層和 Flutter 層之間傳遞命令和回應。
不可避免地,這種同步會產生一定的開銷。因此,一般來說,這種方法最適合像 Google Maps 這樣複雜的控制項,在 Flutter 中重新實作不切實際。
通常,Flutter 應用程式會根據平台測試在 build()
方法中例示這些 Widget。例如,來自 google_maps_flutter 外掛程式:
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
底層的原生程式碼進行通訊,通常使用前面所述的平台通道機制。
目前,平台視圖不適用於桌面平台,但這不是架構上的限制;未來可能會加入支援。
在父應用程式中託管 Flutter 內容
#與先前情境相反的是,將 Flutter Widget 嵌入現有的 Android 或 iOS 應用程式中。如先前章節所述,在行動裝置上執行的新建立的 Flutter 應用程式會託管在 Android Activity 或 iOS UIViewController
中。可以使用相同的嵌入 API 將 Flutter 內容嵌入到現有的 Android 或 iOS 應用程式中。
Flutter 模組範本旨在方便嵌入;您可以將其以來源相依性的形式嵌入到現有的 Gradle 或 Xcode 建置定義中,也可以將其編譯為 Android Archive 或 iOS Framework 二進位檔,以便在不需要每個開發人員都安裝 Flutter 的情況下使用。
Flutter 引擎需要一些時間才能初始化,因為它需要載入 Flutter 共用函式庫、初始化 Dart 執行時期、建立並執行 Dart isolate,以及將渲染表面附加到 UI。為了盡量減少呈現 Flutter 內容時的任何 UI 延遲,最好在整體應用程式初始化順序期間,或至少在第一個 Flutter 畫面之前初始化 Flutter 引擎,這樣使用者就不會在載入第一個 Flutter 程式碼時遇到突然的暫停。此外,分離 Flutter 引擎可以讓它在多個 Flutter 畫面之間重複使用,並共用載入必要函式庫所涉及的記憶體開銷。
有關如何將 Flutter 載入現有的 Android 或 iOS 應用程式的詳細資訊,請參閱 載入順序、效能和記憶體主題。
Flutter 網頁支援
#雖然一般架構概念適用於 Flutter 支援的所有平台,但 Flutter 對網頁的支援有一些獨特的特性值得評論。
Dart 一直以來都可以編譯為 JavaScript,並具有針對開發和生產目的優化的工具鏈。許多重要的應用程式今天都可以從 Dart 編譯為 JavaScript 並在生產環境中執行,包括 Google Ads 的廣告主工具。由於 Flutter 框架是以 Dart 編寫的,因此將其編譯為 JavaScript 相對簡單。
然而,以 C++ 編寫的 Flutter 引擎旨在與底層作業系統而不是網頁瀏覽器介面。因此,需要採用不同的方法。在網頁上,Flutter 在標準瀏覽器 API 的基礎上重新實作引擎。我們目前有兩種選項可將 Flutter 內容渲染到網頁上:HTML 和 WebGL。在 HTML 模式下,Flutter 使用 HTML、CSS、Canvas 和 SVG。若要渲染到 WebGL,Flutter 使用名為 CanvasKit 的 Skia 版本,該版本已編譯為 WebAssembly。雖然 HTML 模式提供最佳的程式碼大小特性,但 CanvasKit
提供通往瀏覽器圖形堆疊的最快路徑,並與原生行動目標提供較高的圖形保真度[4]。
網頁版本的架構層圖如下:
與 Flutter 執行的其他平台相比,最顯著的差異或許在於 Flutter 不需要提供 Dart 執行時期。相反地,Flutter 框架(以及您編寫的任何程式碼)都會編譯為 JavaScript。值得注意的是,Dart 在其所有模式(JIT 與 AOT、原生與網頁編譯)中,幾乎沒有語言語義上的差異,大多數開發人員永遠不會寫出遇到這種差異的程式碼行。
在開發期間,Flutter 網頁會使用 dartdevc
,這是一個支援增量編譯的編譯器,因此允許應用程式進行熱重啟(但目前不支援熱重載)。相反地,當您準備好為網頁建立生產應用程式時,會使用 Dart 的高度優化生產 JavaScript 編譯器 dart2js
,將 Flutter 核心和框架與您的應用程式一起封裝到一個縮小的原始檔中,該檔案可以部署到任何網頁伺服器。程式碼可以以單一檔案的形式提供,也可以透過 延遲匯入 分割為多個檔案。
進一步資訊
#對於那些有興趣了解 Flutter 內部構造的更多資訊的人,深入探討 Flutter 白皮書提供了有關該框架設計理念的實用指南。
除非另有說明,否則本網站上的文件反映了 Flutter 的最新穩定版本。頁面上次更新於 2024-12-01。 檢視原始碼 或 回報問題。