Flutter 架構概述
Flutter 架構的高階概述,包括構成其設計的核心原則和概念。
本文旨在提供 Flutter 架構的高階概述,包括構成其設計的核心原則和概念。 如果您有興趣瞭解如何架構 Flutter 應用,請檢視 架構 Flutter 應用。
Flutter 是一個跨平臺 UI 工具包,旨在允許在 iOS、Android、Web 和桌面等作業系統之間重用程式碼,同時還允許應用程式直接與底層平臺服務互動。 目標是使開發人員能夠交付在不同平臺上感覺自然的應用程式,在存在差異時擁抱差異,同時儘可能地共享程式碼。
在開發過程中,Flutter 應用在 VM 中執行,該 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 使用 Swift 和 Objective-C/Objective-C++,Windows 和 Linux 使用 C++。 使用嵌入器,Flutter 程式碼可以作為模組整合到現有應用程式中,或者該程式碼可能是應用程式的全部內容。 Flutter 包含針對常見目標平臺的多個嵌入器,但還存在其他嵌入器。
Flutter 的核心是 Flutter 引擎,它主要用 C++ 編寫,並支援支援所有 Flutter 應用程式所需的基元。 該引擎負責在需要繪製新幀時柵格化組合場景。 它提供了 Flutter 核心 API 的低階實現,包括圖形文字佈局、檔案和網路 I/O、Dart 執行時和編譯工具鏈。
引擎透過 dart:ui 暴露給 Flutter 框架,它將底層的 C++ 程式碼包裝在 Dart 類中。 該庫暴露了最低級別的基元,例如用於驅動輸入、圖形和文字渲染子系統的類。
通常,開發人員透過 Flutter 框架 與 Flutter 互動,該框架提供了一種現代、響應式框架,用 Dart 語言編寫。 它包括一組豐富的平臺、佈局和基礎庫,由一系列層組成。 從下到上,我們有
- 基本的 基礎 類,以及 動畫、繪畫 和 手勢 等構建塊服務,這些服務提供了對底層基礎的常用抽象。
- 渲染層 提供了一種處理佈局的抽象。 使用此層,您可以構建可渲染物件樹。 您可以動態地操作這些物件,樹會自動更新佈局以反映您的更改。
- Widget 層 是一種組合抽象。 渲染層中的每個可渲染物件在 Widget 層中都有一個對應的類。 此外,Widget 層允許您定義可以重用的類組合。 這是引入響應式程式設計模型的一層。
- Material 和 Cupertino 庫提供了使用 Widget 層組合基元來實現 Material 或 iOS 設計語言的全面控制元件集。
Flutter 框架相對較小;開發人員可能使用的許多更高階的功能都實現為軟體包,包括平臺外掛(如 camera 和 webview),以及平臺無關的功能(如 characters、http 和 animations),這些功能建立在核心 Dart 和 Flutter 庫之上。 其中一些軟體包來自更廣泛的生態系統,涵蓋了諸如 應用內支付、Apple 身份驗證 和 動畫 等服務。
本概述的其餘部分大致向下遍歷各層,從 UI 開發的響應式範例開始。 然後,我們描述瞭如何組合 Widget 並將其轉換為可以作為應用程式的一部分進行渲染的物件。 我們描述了 Flutter 如何在平臺級別與其他程式碼互操作,然後簡要總結了 Flutter 的 Web 支援與其他目標的不同之處。
應用解剖
#下圖概述了由 flutter create 生成的常規 Flutter 應用的組成部分。 它顯示了 Flutter 引擎在此堆疊中的位置,突出顯示了 API 邊界,並標識了各個部分所在的儲存庫。 下面的圖例闡明瞭通常用於描述 Flutter 應用各個部分的術語。
Dart 應用
- 將 Widget 組合成所需的 UI。
- 實現業務邏輯。
- 由應用開發者擁有。
框架 (原始碼)
- 提供構建高質量應用的高階 API(例如,Widget、點選測試、手勢檢測、輔助功能、文字輸入)。
- 將應用的 Widget 樹組合成場景。
引擎 (原始碼)
- 負責柵格化組合場景。
- 提供 Flutter 核心 API 的低階實現(例如,圖形、文字佈局、Dart 執行時)。
- 使用 dart:ui API 將其功能暴露給框架。
- 使用引擎的 Embedder API 與特定平臺整合。
Embedder (原始碼)
- 與底層作業系統協調,以訪問渲染表面、輔助功能和輸入等服務。
- 管理事件迴圈。
- 暴露 平臺特定 API 以將 Embedder 整合到應用中。
Runner
- 將平臺特定 API 的 Embedder 暴露的部分組合成可在目標平臺上執行的應用包。
- 由
flutter create生成的應用模板的一部分,由應用開發者擁有。
響應式使用者介面
#從表面上看,Flutter 是 一種響應式、宣告式 UI 框架,其中開發人員提供應用程式狀態到介面狀態的對映,框架負責在應用程式狀態發生更改時在執行時更新介面。 該模型受到 Facebook 為他們自己的 React 框架所做的工作 的啟發,其中包括對許多傳統設計原則的重新思考。
在大多數傳統的 UI 框架中,使用者介面的初始狀態被描述一次,然後在執行時由使用者程式碼單獨更新,以響應事件。 此方法的一個挑戰是,隨著應用程式的複雜性增加,開發人員需要了解狀態更改如何在整個 UI 中級聯。 例如,考慮以下 UI
狀態可以在許多地方更改:顏色框、色調滑塊、單選按鈕。 當用戶與 UI 互動時,更改必須反映在其他所有地方。 更糟糕的是,除非小心,否則對 UI 的一個小的更改可能會導致對看似無關的程式碼片段的漣漪效應。
對此的一種解決方案是像 MVC 這樣的方法,透過控制器將資料更改推送到模型,然後模型透過控制器將新狀態推送到檢視。 但是,這也有問題,因為建立和更新 UI 元素是兩個可以很容易地不同步的單獨步驟。
Flutter 以及其他響應式框架採用了一種替代方法來解決此問題,即明確地將使用者介面與其底層狀態解耦。 使用 React 風格的 API,您只需建立 UI 描述,框架負責使用該配置來建立和/或根據需要更新使用者介面。
在 Flutter 中,Widget(類似於 React 中的元件)由不可變類表示,這些類用於配置物件樹。 這些 Widget 用於管理佈局的單獨樹,然後用於管理組合的單獨樹。 Flutter 的核心是一系列用於有效地遍歷修改後的樹的部分、將物件樹轉換為較低級別的物件樹以及在這些樹之間傳播更改的機制。
Widget 透過覆蓋 build() 方法來宣告其使用者介面,該方法是一個將狀態轉換為 UI 的函式
UI = f(state)
從設計上講,build() 方法執行速度很快,並且不應有副作用,從而允許框架在需要時呼叫它(可能每渲染幀一次)。
這種方法依賴於語言執行時的某些特性(特別是快速的物件例項化和刪除)。幸運的是,Dart 特別適合這項任務。
元件
#如前所述,Flutter 強調小部件作為組合的單元。小部件是 Flutter 應用使用者介面的構建塊,每個小部件都是使用者介面一部分的不可變宣告。
小部件透過組合形成層次結構。每個小部件巢狀在其父小部件內部,並可以從父小部件接收上下文。這種結構一直延伸到根小部件(託管 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'),
),
],
);
},
),
),
),
);
}
}
在前面的程式碼中,所有例項化的類都是小部件。
應用透過告知框架用另一個小部件替換層次結構中的一個小部件來響應事件(例如使用者互動)來更新其使用者介面。然後,框架比較新舊小部件,並有效地更新使用者介面。
Flutter 有自己對每個 UI 控制元件的實現,而不是依賴於系統提供的控制元件:例如,有一個純 Dart 實現 的 iOS 切換控制元件 和 Android 對應控制元件 的 實現。
這種方法提供了幾個好處
- 提供了無限的可擴充套件性。希望建立 Switch 控制元件變體的開發人員可以以任意方式建立它,而不受作業系統提供的擴充套件點限制。
- 透過允許 Flutter 一次性組合整個場景,而無需在 Flutter 程式碼和平臺程式碼之間來回切換,從而避免了顯著的效能瓶頸。
- 將應用程式行為與任何作業系統依賴項解耦。應用程式在所有版本的作業系統上看起來和感覺都一樣,即使作業系統更改了其控制元件的實現方式。
組合
#小部件通常由許多其他小的、單一用途的小部件組成,這些小部件組合在一起產生強大的效果。
在允許總詞彙量很大的同時,儘可能地將設計概念的數量保持在最低限度。例如,在小部件層中,Flutter 使用相同的核心概念(一個 Widget)來表示繪製到螢幕、佈局(定位和調整大小)、使用者互動、狀態管理、主題、動畫和導航。在動畫層中,一對概念,Animations 和 Tweens,涵蓋了大部分設計空間。在渲染層中,RenderObjects 用於描述佈局、繪製、命中測試和輔助功能。在每種情況下,相應的詞彙量最終會很大:有數百個小部件和渲染物件,以及數十種動畫和 tween 型別。
類層次結構故意淺而寬,以最大限度地提高可能的組合數量,專注於小型、可組合的小部件,每個小部件都做好一件事情。核心功能是抽象的,即使是填充和對齊等基本功能也是作為單獨的元件實現的,而不是內建到核心中。(這與更傳統的 API 形成對比,在這些 API 中,填充等功能內建到每個佈局元件的通用核心中。)因此,例如,要居中一個小部件,而不是調整一個概念上的 Align 屬性,您可以將其包裝在 Center 小部件中。
有用於填充、對齊、行、列和網格的小部件。這些佈局小部件沒有自己的視覺表示。相反,它們的唯一目的是控制另一個小部件的佈局的某些方面。Flutter 還包括利用這種組合方法的實用小部件。
例如,Container,一個常用的小部件,由幾個負責佈局、繪製、定位和調整大小的小部件組成。具體來說,Container 由 LimitedBox、ConstrainedBox、Align、Padding、DecoratedBox 和 Transform 小部件組成,您可以閱讀其原始碼來檢視。Flutter 的一個定義性特徵是,您可以深入到任何小部件的原始碼中並檢查它。因此,與其繼承 Container 以生成自定義效果,您可以以新穎的方式組合它和其他小部件,或者只是使用 Container 作為靈感來建立一個新小部件。
構建 Widget
#如前所述,您透過重寫 build() 函式來返回一個新的元素樹來確定小部件的視覺表示。這個樹以更具體的方式表示小部件的使用者介面的一部分。例如,工具欄小部件可能有一個構建函式,該函式返回一個 水平佈局 的一些 文字 和 各種 按鈕。根據需要,框架遞迴地要求每個小部件構建,直到該樹完全由 具體的渲染物件 描述。然後,框架將可渲染物件縫合在一起形成一個可渲染物件樹。
小部件的構建函式應該沒有副作用。每當要求該函式構建時,該小部件應該返回一個新的小部件樹1,無論該小部件之前返回什麼。框架會完成大量工作來確定基於渲染物件樹(稍後詳細介紹)需要呼叫哪些構建方法。有關此過程的更多資訊,請參見 Inside Flutter 主題。
在每個渲染幀上,Flutter 可以透過呼叫該小部件的 build() 方法重新建立 UI 中狀態發生更改的部分。因此,構建方法應該快速返回,並且繁重的計算工作應該以某種非同步方式完成,然後儲存為狀態的一部分,供構建方法使用。
雖然方法相對簡單,但這種自動比較非常有效,能夠實現高效能、互動式應用。而且,構建函式的設計簡化了您的程式碼,重點在於宣告小部件由什麼組成,而不是從一個狀態更新到另一個狀態的使用者介面的複雜性。
Widget 狀態
#框架引入了兩種主要型別的小部件:有狀態 和 無狀態 小部件。
許多小部件沒有可變狀態:它們沒有任何隨時間變化的屬性(例如,圖示或標籤)。這些小部件繼承自 StatelessWidget。
但是,如果小部件的獨特特徵需要根據使用者互動或其他因素而變化,則該小部件是有狀態的。例如,如果一個小部件有一個計數器,每當使用者點選按鈕時都會遞增,那麼計數器的值就是該小部件的狀態。當該值更改時,需要重新構建小部件以更新其 UI 的一部分。這些小部件繼承自 StatefulWidget,並且(因為小部件本身是不可變的)它們將可變狀態儲存在繼承自 State 的單獨類中。StatefulWidgets 沒有構建方法;相反,它們的使用者介面透過其 State 物件構建。
每當您更改 State 物件(例如,透過遞增計數器)時,必須呼叫 setState() 以向框架發出訊號,透過再次呼叫 State 的構建方法來更新使用者介面。
將狀態和小部件物件分開,讓其他小部件以完全相同的方式對待無狀態和小部件,而無需擔心丟失狀態。與其需要保留子物件以保留其狀態,父物件可以隨時建立子物件的新的例項,而不會丟失子物件的持久狀態。框架會完成所有工作,並在適當的時候找到並重用現有的狀態物件。
狀態管理
#那麼,如果許多小部件可以包含狀態,那麼狀態是如何管理和在系統中傳遞的呢?
與任何其他類一樣,您可以使用建構函式在小部件中初始化其資料,因此 build() 方法可以確保任何子小部件都使用其需要的資料例項化。
@override
Widget build(BuildContext context) {
return ContentWidget(importantState);
}
其中 importantState 是一個佔位符,表示包含 Widget 狀態的類。
但是,當小部件樹變得更深時,在樹層次結構中上傳遞狀態資訊會變得繁瑣。因此,第三種小部件型別,InheritedWidget,提供了一種從共享祖先獲取資料的一種簡單方法。您可以使用 InheritedWidget 建立一個狀態小部件,該小部件包裝小部件樹中的一個公共祖先,如下所示
每當 ExamWidget 或 GradeWidget 物件中的一個需要來自 StudentState 的資料時,它現在可以使用如下命令訪問它
final studentState = StudentState.of(context);
of(context) 呼叫獲取構建上下文(對當前小部件位置的控制代碼),並返回與 StudentState 型別匹配的樹中最近的祖先。InheritedWidget 還提供了一個 updateShouldNotify() 方法,Flutter 會呼叫該方法來確定狀態更改是否應該觸發使用它的子小部件的重建。
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(用 C/C++ 編寫的圖形引擎)來呼叫 CPU 或 GPU 以在裝置上完成繪製。
跨平臺框架通常透過在底層原生 Android 和 iOS UI 庫之上建立一個抽象層來工作,試圖平滑每個平臺表示形式的不一致之處。應用程式程式碼通常是用 JavaScript 等解釋型語言編寫的,而 JavaScript 必須與基於 Java 的 Android 或基於 Objective-C 的 iOS 系統庫互動才能顯示 UI。所有這些都會增加開銷,尤其是在 UI 和應用程式邏輯之間有很多互動的情況下。
相比之下,Flutter 儘量減少這些抽象,繞過系統 UI 元件庫,而選擇使用自己的元件集。繪製 Flutter 視覺化的 Dart 程式碼被編譯成原生程式碼,該程式碼使用 Impeller 進行渲染。Impeller 與應用程式一起釋出,允許開發者升級他們的應用程式,以便及時獲得最新的效能改進,即使手機尚未更新到新的 Android 版本。對於 Flutter 在其他原生平臺(如 Windows 或 macOS)上的應用也是如此。
從使用者輸入到 GPU
#Flutter 應用於其渲染管道的 overriding 原則是 簡單即快速。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 和約束。一些簡要的平臺特定說明
- 截至 Flutter 3.29,UI 和平臺執行緒在 iOS 和 Android 上合併。具體來說,刪除了 UI 執行緒,Dart 程式碼在原生平臺執行緒上執行。有關更多資訊,請參閱 The great thread merge 影片。
- 在 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 到廣告再到相機和藍牙等裝置硬體。
外部函式介面 (FFI)
#對於基於 C 的 API,包括可以為用現代語言(如 Rust 或 Go)編寫的程式碼生成的 API,Dart 提供了使用 dart:ffi 庫繫結到本機程式碼的直接機制。 外來函式介面 (FFI) 模型可能比平臺通道快得多,因為無需序列化資料。 相反,Dart 執行時提供了在堆上分配由 Dart 物件支援的記憶體,並呼叫靜態或動態連結庫的能力。 FFI 可用於所有平臺,除了 Web 平臺,後者使用 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 內容繪製到紋理上,並且其小部件樹完全是內部的,因此 Android 檢視之類的東西無法存在於 Flutter 的內部模型中或與 Flutter 小部件交錯渲染。 這對於希望在 Flutter 應用中包含現有平臺元件(如瀏覽器控制元件)的開發者來說是一個問題。
Flutter 透過引入平臺檢視小部件(AndroidView 和 UiKitView)來解決這個問題,這些小部件允許你在每個平臺上嵌入這種型別的內容。 平臺檢視可以與其他 Flutter 內容整合3。 這些小部件中的每一個都充當與底層作業系統的中間媒介。 例如,在 Android 上,AndroidView 具有三個主要功能
- 複製本機檢視渲染的圖形紋理,並在每次繪製幀時將其呈現給 Flutter 以作為 Flutter 渲染表面的組成部分。
- 響應點選測試和輸入手勢,並將這些手勢轉換為等效的本機輸入。
- 建立可訪問性樹的類似物,並在本機和 Flutter 層之間傳遞命令和響應。
不可避免的是,與這種同步相關聯的開銷。 因此,通常情況下,這種方法最適合於複雜的控制元件,例如 Google Maps,在這種情況下,在 Flutter 中重新實現是不切實際的。
通常,Flutter 應用會在基於平臺測試的 build() 方法中例項化這些小部件。 例如,來自 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 小部件嵌入到現有的 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 Web 支援
#雖然通用的架構概念適用於 Flutter 支援的所有平臺,但 Flutter 的 Web 支援有一些獨特的特性值得評論。
Dart 已經編譯為 JavaScript 只要該語言存在,並具有針對開發和生產目的最佳化的工具鏈。 許多重要的應用都從 Dart 編譯為 JavaScript 並在生產環境中執行,包括 Google Ads 的廣告商工具。 由於 Flutter 框架是用 Dart 編寫的,因此將其編譯為 JavaScript 相對簡單。
但是,Flutter 引擎是用 C++ 編寫的,旨在與底層作業系統而不是 Web 瀏覽器進行互動。 因此,需要採用不同的方法。
在 Web 上,Flutter 提供兩種渲染器
| 渲染器 | 編譯目標 |
|---|---|
| CanvasKit | JavaScript |
| Skwasm | WebAssembly |
構建模式是命令列選項,用於指定在執行應用時可用的渲染器。
Flutter 提供兩種構建模式
| 構建模式 | 可用渲染器 |
|---|---|
| default | CanvasKit |
| `--wasm` | Skwasm(首選),CanvasKit(備用) |
預設模式僅使 CanvasKit 渲染器可用。 --wasm 選項使兩種渲染器可用,並根據瀏覽器功能選擇引擎:如果瀏覽器能夠執行它,則優先選擇 Skwasm,否則回退到 CanvasKit。
與其他 Flutter 執行的平臺相比,最值得注意的區別是 Flutter 無需提供 Dart 執行時。 相反,Flutter 框架(以及你編寫的任何程式碼)都會編譯為 JavaScript。 值得注意的是,Dart 在所有模式(JIT 與 AOT、本機與 Web 編譯)中都很少有語言語義差異,並且大多數開發者永遠不會編寫遇到這種差異的程式碼。
在開發期間,Flutter Web 使用 dartdevc,這是一種支援增量編譯的編譯器,因此允許熱重啟和 [帶有標誌的熱過載][]。 相反,當你準備為 Web 建立生產應用時,將使用 Dart 的高度最佳化的生產 JavaScript 編譯器 dart2js,將 Flutter 核心和框架與你的應用程式打包到可以部署到任何 Web 伺服器的縮小原始檔中。 程式碼可以透過 延遲匯入 提供為單個檔案或拆分為多個檔案。
有關 Flutter Web 的更多資訊,請檢視 Flutter 的 Web 支援 和 Web 渲染器。
更多資訊
#對於那些對 Flutter 內部結構感興趣的人,Inside Flutter 白皮書提供了框架設計理念的有用指南。