【文章翻譯】Introducing the Desktop Windowing API for Flutter

【文章內容使用 Gemini 2.5 Flash 自動翻譯產生】

原文:https://flutter.dev/blog/desktop-windowing-apis

介紹 Flutter 的桌面視窗化 API

了解新桌面視窗化 API 的設計,並編寫您的第一個多視窗 Flutter 應用程式。

Flutter 最初於 2018 年發布,支援兩個主要的行動平台:Android 和 iOS。從那時起,Flutter 的應用範圍已遠遠超出行動裝置,除了汽車中的資訊娛樂系統、智慧電視和嵌入式系統等自訂用例之外,還正式支援 Web、macOS、Linux 和 Windows。從行動應用程式工具包的卑微開端,Flutter 已成長為跨越廣泛裝置的第一流應用程式工具包。

但是,Flutter 作為行動工具包的一些遺留痕跡至今仍然存在。行動開發的一個特點是您不必繪製到多個視窗—視窗只佔據整個螢幕。與其獨立繪製特殊的「視窗」,例如彈出視窗和工具提示,不如將它們渲染為疊加在主視窗之上的圖層。對於行動用例,這是一個非常好的權衡。外形尺寸足夠小,以至於擁有多個獨立視窗對應用程式開發人員來說在可用性方面幾乎沒有任何好處。

然而,一旦 Flutter 發佈用於桌面,這種單視窗假設就成了一個問題。為了讓應用程式在桌面上感覺自在,它們必須利用大螢幕顯示器提供的螢幕空間。應用程式應該能夠根據需要繪製到任意數量的視窗中,更重要的是,甚至超出單個視窗的邊界進行繪製。當時,多視窗支援是 Flutter 中第六個最受歡迎的功能,這進一步證明了此功能的重要性。

當這一切發生時,我們 Canonical 正在忙於將 Flutter 整合到我們堆疊的關鍵任務部分,包括 Ubuntu 桌面安裝程式。此時我們已經承擔了 Flutter Linux Embedder 主要維護者的責任,並且我們越來越對 Flutter 中視窗化 API 的原生支援感興趣。因此,在 2024 年,我們決定與 Google 合作開發這個新的 API,由 Canonical 負責設計和實作,Google 則提供設計和程式碼審查支援。

視窗類型和階層

視窗化 API 的一個主要設計考量是如何使其跨平台。當應用程式開發人員可以自信地「一次編寫,隨處發布」時,Flutter 才能發揮最佳效能,我們希望設計一個能讓這件事盡可能容易的 API。這就是我們得出**視窗類型**概念的方式。

我們的設計圍繞五種視窗類型:常規、對話框、工具提示、彈出視窗和衛星。每種視窗類型都有特定的跨平台行為和用途。

**彈出**視窗提供下拉選單和文字自動完成框等功能。它們是其他視窗的子視窗,通常會根據其內容調整大小。重要的是,它們可以接收輸入焦點(例如,使用者可以使用箭頭鍵瀏覽下拉選單)。我們的實作還強制彈出視窗透過平移或縮小來保持在螢幕上可見,從而確保重要資訊永遠不會在螢幕外丟失。

**工具提示**視窗類似於彈出視窗,只是它們無法獲得輸入焦點。它們通常用於少量、轉瞬即逝的資訊。例如,信用卡資訊的輸入表單通常會包含一個 CVV 號碼輸入框,旁邊有一個小小的「資訊」圖示。當滑鼠懸停時,此圖示會彈出一個工具提示,解釋此資訊在您的卡片上的位置。此類小型的資訊視窗最適合實作為工具提示。

**對話框**視窗是子視窗,通常提示使用者採取行動。對話框視窗有兩種:模態和非模態。當對話框對另一個視窗是模態時,在對話框視窗關閉之前,該視窗無法獲得焦點。對話框視窗的一個常見範例是模態提示,要求使用者確認或取消刪除應用程式資源。

**衛星**視窗是輔助視窗,用於工具箱和其他輔助功能。它們的獨特之處在於,它們會隨著父視窗的移動和調整大小而保持相對於父視窗的位置。它們也可能同時是多個視窗的子視窗,而不僅僅是一個視窗,這樣工具欄就可以在多個主視窗之間共用。衛星通常也可以停靠,這意味著它們可以從浮動衛星視窗轉換為嵌入到主應用程式內容中。衛星視窗的一個範例是圖像編輯軟體(如 GIMP)中包含顏色選擇、工具選擇等的工具箱。

這些視窗類型存在於視窗階層中。例如,應用程式可以在其根目錄中包含一個主要、常規的視窗,該視窗具有作為子視窗的彈出視窗和對話框。然後,對話框可以在其內部嵌套一個工具提示。應用程式然後可以打開另一個根目錄中的常規視窗,並在該視窗內部嵌套另一個彈出視窗。透過這種方式,構建了一個視窗階層。實際上,這個階層應該相當淺,但是您不應因具有深層階層而產生任何效能成本。

這個設計並非武斷選擇。Canonical 在過去二十年大部分時間都在發布具有各種複雜應用程式的桌面。透過我們的研究,我們發現這五種主要類型在大多數應用程式中都是基礎,因此應該在此 API 中獲得一流的支援。最棒的是,作為開發人員,您可以放心地在所有主要桌面平台上發布這些視窗類型,因為您知道行為將始終一致。

然而,我們認識到確實會出現獨特的情況。因此,我們始終提供一個「逃生艙口」,您可以透過它為您的特定平台建構和修改自己的原生視窗。雖然這不是推薦的方法,但在需要時它可能是一個強大的工具。

探索 API

視窗化 API 目前可在 Flutter 「main」頻道上使用。請注意,這些 API 隱藏在一個實驗性功能標誌之後,因此可能會更改。

若要啟用視窗化 API,請運行以下命令:

flutter channel main
flutter upgrade
flutter config --enable-windowing

建立和修改原生視窗

若要建立視窗,我們首先建立一個 `WindowController`。控制器與底層平台互動以建立和更新視窗。

dart
final controller = WindowController(
 title: 'My Application',
 size: const Size(800, 600),
);

控制器提供了視窗的初始配置,在本例中包括大小和標題。請注意,這些引數可能不會被平台遵守。例如,如果螢幕不夠大以容納請求的大小,平台可以選擇較小的視窗大小。

每種視窗類型都有自己的視窗控制器,它接收不同的參數。例如,要建立一個對話框視窗而不是一個常規視窗,看起來像這樣:

dart
final dialogController = DialogWindowController(
 title: 'My Dialog',
 size: const Size)(400, 300),
 parent: parentController,
);

與常規視窗不同,DialogWindowController 在其建構函式中接受一個可選的父視窗控制器。

一旦控制器建立,以後可以用它來修改視窗。例如,我們可以修改之前建立的常規視窗的標題或大小,甚至銷毀它:

dart
controller.setTitle('Hello, world!');
controller.setSize(const Size.square)(1000));
controller.destroy)();

渲染到原生視窗中

現在我們知道如何建立和更新我們的原生視窗了,是時候在裡面渲染一些東西了。為此,我們將控制器和要渲染的內容傳遞給 `Window` Widget:

dart
Widget build(BuildContext context) {
 return Window(
 controller: controller,
 child: MyPage(),
);
}

每種視窗類型都有其對應的 Widget。對於常規視窗,我們使用 Window Widget,但對於對話框視窗,我們將使用 DialogWindow Widget。

值得注意的是,所有視窗都在一個 Widget 樹中。這意味著您可以無需任何額外工作即可在視窗之間共享狀態。現有的狀態管理套件(如 Riverpod 或 Bloc)可以直接使用!

監聽原生視窗上的事件

現在我們知道如何建立原生視窗並在其中渲染內容了,還有一個難題需要解決:我們如何從系統接收關於視窗的通知?這可以透過兩種方式實現:透過視窗控制器委派和 WindowScope

WindowControllerDelegate 會通知我們重要的生命週期事件,例如視窗被銷毀或系統要求我們銷毀視窗。我們透過覆蓋委派控制器來實現委派。例如:

dart
// Create the class first...
class MyWindowDelegate with WindowControllerDelegate {
 @override
 void onWindowDestroyed() {
 super.onWindowDestroyed)();
 ServicesBinding.instance.exitApplication)(AppExitType.required);
 }
}

// and then pass it to the controller constructor.
final controller = WindowController(
 title: 'My Application',
 size: const Size)(800, 600),
 delegate: MyWindowDelegate)(),
);

此委派在視窗被銷毀時退出應用程式,這通常是應用程式主要視窗的適當實作。

我們可以借助 WindowScope 監聽有關視窗的不那麼關鍵的資訊,這是每個 Window Widget 建立的 InheritedModel。嵌套在視窗中的 Widget 可以使用 WindowScope.of 來存取範圍。或者,它們可以使用 WindowScope 上各種特定欄位的存取器來監聽視窗的特定欄位。例如,如果 Widget 對視窗的標題感興趣,它可以這樣做:

dart
class MyWidget extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
 final title = WindowScope.titleOf)(context);
 // ... do something with the window title
 }
}

每當視窗標題變更時,這個 Widget 現在都會重新渲染。

「Hello, Window」範例

現在我們了解了 API 及其背後的原理,以下這個獨立的「Hello, Window」範例應用程式應該完全可以理解。請隨意複製並貼上到您自己的 Flutter 專案中,自行嘗試:

dart
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: implementation_imports

import 'dart:ui';

import 'package:flutter/services.dart';
import 'package:flutter/src/widgets/_window.dart';
import 'package:flutter/widgets.dart';

/// Exits the application when the user closes the window.
class ExitOnCloseDelegate with WindowControllerDelegate {
 @override
 void onWindowCloseRequested(WindowController controller) {
 ServicesBinding.instance.exitApplication)(AppExitType.required);
 }
}

void main() 下載 {
 WidgetsFlutterBinding.ensureInitialized)();
 runWidget)(const HelloWindow)();
}

/// Displays a window and owns its [WindowController].
class HelloWindow extends StatefulWidget {
 const HelloWindow({super.key});

 @override
 State<HelloWindow> createState() => _HelloWindowState)();
}

class _HelloWindowState extends State<HelloWindow> {
 final WindowController _controller = WindowController(
 size: const Size)(600, 400),
 title: 'MyApp',
 delegate: ExitOnCloseDelegate)(),
);

 @override
 void dispose() >{
 _controller.dispose)();
 super.dispose)();
 }

 @override
 Widget build(BuildContext context) >{
 return Window(
 controller: _controller,
 child: const Directionality(
 textDirection: TextDirection.ltr,
 child: ColoredBox(
 color: Color(0xFFFFFFFF),
 child: Center(
 child: Text(
 'Hello, Window下載,
 style: TextStyle(color: Color(0xFF000000), fontSize: 24),
 ),
 ),
 ),
 );
 }
}

這個範例最棒的地方在於,它無需您額外的工作,即可在所有主要的桌面平台開箱即用。

關於上述範例,需要注意的一件重要事情是,我們不再使用 runApp 函數來渲染我們的根 Widget,因為該函數會將 Widget 附加到由平台隱式建立的視圖。相反,我們使用 runWidget 並向其提供我們自己的視圖,即我們的 Window

設計系統整合

需要注意的一個重要點是,從長遠來看,大多數視窗化 API 的使用者不會明確地與其互動。相反,它將由現有的設計系統在幕後整合。透過選擇設計系統,您將開箱即用地獲得所有精美的視窗功能。

具體來說,團隊目前正努力將新的視窗化 API 整合到 Material 設計系統中。我們計劃整合的項目有:

當平台提供這些方法時,它們都將使用真正的原生視窗。當平台上無法使用視窗化 API 時(例如行動裝置),我們將回溯到現有的實作。該 API 還將提供在需要時完全退出視窗化的方法。

結論

視窗化 API 是 Canonical 和 Google 多年設計和工程的成果,我們非常興奮最終能將其交到您的手中。如果您有興趣查看更複雜的多視窗應用程式是什麼樣子,請查看https://flutter.dev/to/windowing-example

我們期待看到您的作品!

更多 Flutter 資訊