← Writing
Flutter 技術 · · 11 min read · Read in English

Flutter 3.50 & Dart 3.14 重點整理

這一波 Flutter 3.50 + Dart 3.14 把 Material/Cupertino 從框架拆出去、Native interop 宣告穩定、iOS 27 正式支援,同時 A2UI 1.0、on-device AI、桌面多視窗一起登場。

一頁看懂#

影片(上架日)一句話重點誰該看
The Future of Flutter’s Dev Experience(11/11)MCP 給工具、skills 給食譜、evals 驗證 AI 寫的 Flutter 是否道地所有用 AI 寫 code 的人
The power of Dart & Wasm(11/11)Safari / Firefox 可選用 Wasm、SIMD 加速、--web-content-hash 原子部署Flutter Web
Build a webOS app with Flutter(11/11)LG 電視官方 SDK、hot reload 上電視、pluggable platforms 計畫新平台、嵌入式
What A2UI 1.0 means for Flutter devs(11/11)巢狀護欄、Express 格式(10 秒→5 秒)、多 catalog、agentFunction做 AI 助理 / GenUI
On-device AI with LiteRT-LM and Gemma(11/11)離線、隱私、零 token 成本的本機 LLM,含聊天與 tool calling想加 AI 功能但怕成本/隱私
Google AI Edge: local AI desktop app in 10 days(11/11)Eloquent 10 天從 iOS 到桌面;新 app 重用 60% 基礎、省 80% 時間PM、技術主管
Flutter interop with Pigeon FFI(11/11)useJni: true:保留 Pigeon 型別合約,改走 JNI 直呼,免序列化寫 plugin / 原生橋接
Custom design libraries with Flutter(11/11)Raw widgets + Mix/Remix 樣式層 + Widgetbook 視覺回歸設計系統、UI 團隊
Multi-window on desktop(11/11)實驗性多視窗 API,五種視窗類型,單一 widget tree 共享狀態桌面 app
What’s new in Flutter 3.50 and Dart 3.14(11/11)版本總覽:dart migrate、material_ui、iOS 27、SwiftPM、hybrid composition所有人
Client side functions for Flutter GenUI(11/05)讓 LLM 把算錢這類工作交給本機 Dart 函式,結果可驗證、無延遲做 AI 助理 / GenUI

Dart 3.14:沒有新語法,但升級變自動了#

影片:What’s new in Flutter 3.50 and Dart 3.14(Dart 3.14 0:00)

Dart 3.14 不加語言功能(this promotion 與 static extensions 還在做),重點是工具與效能。

1. dart migrate:自動升 SDK 下限 + 套用新寫法

每次把 pubspec 的 SDK 下限往上推一個 minor,分三步:prepare(先跑 bump 前需要的 dart fix)→ bump → cleanup(可選)。需要原本已是 Dart 3.0+。

dart migrate --dry-run                       # 先看會改什麼
dart migrate --apply                         # 一次做完
dart migrate --apply --step=prepare          # 或分步,方便拆 PR
dart migrate --apply --step=bump,cleanup
dart migrate --apply --target-sdk=3.14.0

官方範例(前 → 後),可以看到空 body 的新寫法與參數 final 被拿掉:

// before
class WeakPasswordException implements Exception {}
void checkPassword(final String password, final int minLength) {

// after
class WeakPasswordException implements Exception;
void checkPassword(String password, int minLength) {

2. Analysis Server 變快:檔案探索(Find References)、啟動與重啟、documentColor 都更快,格式化時較少卡住。在 16 個 context 的 DevTools workspace 裡,冷啟動 rename 從 2.0 秒降到 0.9 秒(約快 55%)——你的 IDE 與 AI agent 都受惠。

3. Pub 用 ETag:get / upgrade / add 帶 If-None-Match,144 個依賴的專案約省 1.5 MB 下載,CI 會感受得到。

4. DartPad beta 全在瀏覽器內:編譯器、analyzer、pub 都編成 Wasm 跑在 Web Worker,支援多檔案、import pub.dev 套件、完整 LSP,沒有網路來回。

5. 其他值得知道的改動

  • 新 API rangeEquals(整數 typed data 與 ByteData),VM 上直接變 memcmp:
final pngMagic = Uint8List.fromList([0x89, 0x50, 0x4E, 0x47]);
if (packet.rangeEquals(0, 4, pngMagic)) print('PNG header matched');
  • dart:html、dart:js、dart:js_util、package:js 正式棄用並排定移除,改用 dart:js_interop + package:web。
  • dart:mirrors 棄用;新增 dart run -e(一行跑 Dart)、dart pub upgrade pkg@latest、lint future_sync_value。
  • SIMD 型別(Int32x4、Float32x4…)在 Wasm 編成 v128,benchmark 最高快 4 倍。

情境延伸:多 package 的 monorepo(app + 共用 domain + design system)可以每個 package 各跑 dart migrate --step=prepare 開一支小 PR,再統一 bump,review 壓力小很多;--dry-run 的輸出也很適合丟給 AI agent 當改動清單。

延伸閱讀

Flutter 3.50 框架:Material / Cupertino 正式搬出 SDK#

影片:3.50 總覽:Framework updates 1:59

最大的變化是 package:flutter/material.dart 與 cupertino.dart 正式 deprecated,改用獨立套件 material_ui、cupertino_ui(每週發版、semver)。flutter create 預設已 import material_ui。時間線:3.44 凍結 SDK 內版本(= 套件 1.0.0)→3.47 可選用 → 3.50 棄用。

官方 blog:從 SDK 內建的 material.dart / cupertino.dart 遷移到 material_ui / cupertino_ui

遷移步驟

  1. dart fix --apply --code=migrate_design_widgets(加依賴、改 import)
  2. 若 pubspec 沒被更新:flutter pub add material_ui cupertino_ui
  3. import 改為 package:material_ui/material_ui.dart / package:cupertino_ui/cupertino_ui.dart
  4. 再跑一次 dart fix(import 排序等)

第三方套件還在用舊 import 怎麼辦?用 MaterialUiCompatibilityBridge(Cupertino 對應 CupertinoUiCompatibilityBridge)把新 theme 轉成舊版 theme、並注入在地化。它本身已標 @Deprecated——只是過渡工具,依賴套件遷移完就要拿掉。型別不合的情況(例如把 SDK 的 ColorScheme 傳給 material_ui API)它救不了:

import 'package:material_ui/material_ui.dart';

MaterialApp(
  theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2E7D32))),
  // 整個 app 包一次;也可以只包住使用舊套件的那個子樹
  // ignore: deprecated_member_use
  builder: (context, child) => MaterialUiCompatibilityBridge(child: child!),
  home: const HomeScreen(),
);

套件作者注意:遷移對使用者是 breaking change,要發 major 版。

新 widget 與 API(挑實用的)

類別新東西用在哪
SliverSliverClipRect、SliverClipRRect(clipOverlap)卡片式列表圓角裁切、跟 pinned header 疊加
列表ListView.separated 的 itemExtentBuilder獲得固定高度優化,又能有分隔線
圖片DecorationImage.placeholder、FadeInImageTransition.fadeInOver、ResizeImage.useLogicalSize頭像 / 商家 logo 載入前的佔位圖
輸入AutofillHints.emailOTPCode、數字鍵盤 password(Android)、HyphensEmail OTP 自動填入、PIN 碼
無障礙SemanticsRole.slider、MediaQueryData.reduceMotion、Esc 關 Tooltip尊重系統「減少動態」
導覽Navigator.popUntilWithResult、showDialog(barrierBuilder:)多步驟流程結束時帶結果回到起點

左:SliverClipRRect 把捲動中的 sliver 裁成圓角;右:DecorationImage.placeholder 先顯示模糊佔位圖(官方 blog)

Breaking changes:IgnorePointer / AbsorbPointer 的 ignoringSemantics 移除;軟連字號在換行處會顯示「-」;RTL 文字中 WidgetSpan 依自然閱讀順序;Web viewport-fit=cover 時 SafeArea 會算進瀏海。

情境延伸

  • popUntilWithResult:報名流程(選班 → 確認條款 → 上傳證件)結束後直接回到工作列表並帶回「已報名」,不用再靠全域事件通知列表重整。
  • itemExtentBuilder:長列表跳到第 N 筆(例如「今天」)不用先建立前面所有 item。
  • reduceMotion:系統開了減少動態就關掉 Lottie / 轉場動畫。

延伸閱讀

平台更新:iOS 27、Android 安全預設、Engine#

影片:iOS 3:05、Android 4:30、Engine 5:17

iOS / macOS

  • 3.50 是 Xcode 27 / iOS 27 之後第一個 stable,要支援 iOS 27 使用者就升級。新配對的裝置會自動準備好;flutter screenshot 在實機上又能用了(讓 AI agent 自己看改動前後畫面)。也支援新的 iPhone duo 摺疊形態。
  • 觸控延遲:60 Hz 裝置少 16 ms、120 Hz ProMotion 少 8 ms,測試覆蓋到 iPhone 6s。
  • CocoaPods trunk 2026 年 12 月變唯讀。關掉 Swift Package Manager 的專案,現在每個 CLI 指令都會跳警告。
  • Intel Mac 建置會跳棄用警告,未來版本移除;可用 flutter config --enable-macos-arm64-only。

官方 blog:iOS 觸控排程少一整個 frame 的延遲

Android

  • Release 版 Intent 加固:忽略 route、dart_entrypoint、cached_engine_id extras,拒絕經 Intent 或 --use-application-binary 傳入的 engine flags(如 --dart-flags)。debug 版只警告。如果你們用 Intent extra 帶 initial route 做 deep link,要改走 go_router / app links。
  • Hybrid Composition++:把 platform view 合成交給 Android OS,解決捲動撕裂與文字輸入問題。需 Android 14(API 34)+ 且 Impeller 走 Vulkan,不符合自動回退。beta 已預設開啟,stable 要手動 flutter config --enable-hcpp,看 log 出現 Using HCPP platform view rendering strategy 即生效。地圖、WebView、廣告多的 app 優先測。
  • Built-in Kotlin:plugin 自己 apply Kotlin Gradle Plugin 很快會編譯失敗,要改用內建 Kotlin 並宣告 flutter: ">=3.44.0"。
  • 新專案預設:AGP 9.3.1、Gradle 9.5.0、KGP 2.4.20、Java 17;compile / target SDK = API 36,minSdk = API 24。

Engine

  • Impeller SDF 渲染(桌面)支援線條、細邊框、圓角、漸層,macOS 漸層與重複文字陰影變快。
  • 自訂 embedder:Vulkan external textures、FlutterEngineNotifyLowMemoryWarning(低記憶體時清 Impeller 離屏快取)。
  • Flutter GPU 支援 2D texture arrays、shader pipeline 錯誤訊息更清楚。
  • Windows 預設要 OpenGL ES 3、支援非 ASCII 路徑(中文資料夾名稱不再出事)。

官方 blog:Impeller SDF vs. MSAA 像素放大對照

延伸閱讀

Native interop:FFIgen / JNIgen 穩定,Pigeon 直連(preview)#

影片:Flutter interop with Pigeon FFI(三種方式比較 1:05、定義 @HostApi 2:00、實機權限流程 4:05)、3.50 總覽:Native interop 1:13

FFIgen、JNIgen 進入 stable

  • FFIgen / JNIgen 正式穩定:從 C / Objective-C / Swift 標頭與 Java / Kotlin 類別直接產生 Dart 綁定,同步呼叫、不經 platform channel。
  • Pigeon 30 的 native interop 模式(preview):同一份 Pigeon 定義,Android 走 JNI(useJni: true)、iOS / macOS 走 FFI(useFfi: true),不再序列化到 MethodChannel。目前不支援 Windows / Linux,官方明說不要用在 production。

Dart bindings:FFI / JNI 直接呼叫原生函式庫

Pigeon runtime:MethodChannel 序列化 vs. JNI / FFI 直呼

官方 blog:MethodChannel 序列化 vs. JNI / FFI 直呼

Pigeon 定義(pigeon 30.0.2,對照官方 native_interop_guide)

// pigeons/messages.dart
import 'package:pigeon/pigeon.dart';

@ConfigurePigeon(PigeonOptions(
  appDirectory: './',
  dartOut: 'lib/src/messages.g.dart',
  kotlinOut: 'android/src/main/kotlin/com/example/permissions/Messages.g.kt',
  kotlinOptions: KotlinOptions(package: 'com.example.permissions', useJni: true),
  swiftOut: 'ios/Classes/Messages.g.swift',
  swiftOptions: SwiftOptions(useFfi: true, ffiModuleName: 'permissions'),
))
@HostApi()
abstract class PermissionsApi {
  bool checkPermission(String permission);
  bool checkAndRequestPermission(String permission);
}
flutter pub add dev:pigeon
# Android(JNI)
flutter pub add jni && flutter pub add dev:jnigen dev:logging dev:path
# iOS / macOS(FFI)
flutter pub add ffi objective_c meta && flutter pub add dev:ffigen dev:swift2objc dev:swiftgen dev:path dev:pub_semver
# 需先設好 ANDROID_HOME,並在 example/android 跑過一次 ./gradlew compileReleaseKotlin
dart run pigeon --input pigeons/messages.dart

產出:messages.g.dart、messages.native_interop.g.dart,以及 messages.g.jni.dart / messages.g.ffi.dart。

// Dart 端:改用 native interop 建構子
late final PermissionsApi _api = PermissionsApi.createWithNativeInteropApi();
final granted = await _api.checkPermission('camera');
// Android plugin
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
  PermissionsApiRegistrar().register(this, binding)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
  PermissionsApiRegistrar().unregister(this, binding)
}
// iOS plugin
public static func register(with registrar: FlutterPluginRegistrar) {
  let instance = PermissionsPlugin()
  PermissionsApiSetup.register(api: instance, registrar: registrar)
  registrar.publish(instance)
}

Pigeon FFI 範例 app:檢查並請求相機權限

影片中的範例 app:按鈕直接經 JNI 呼叫 Kotlin,跳出系統權限視窗後立刻更新狀態。

情境延伸

  • 頻繁的小呼叫(權限檢查、裝置資訊、電量 / 網路狀態)最受益:少了序列化與 thread hop。
  • 大型 SDK(推播、金流、地圖)先維持現有 plugin;等 interop 模式脫離 preview 再評估。
  • 團隊自寫 plugin 可以先在 example app 試 useJni / useFfi,量測冷啟動與呼叫延遲,再決定是否遷移。

延伸閱讀

桌面多視窗 API(實驗性,僅 main channel)#

影片:Multi-window on desktop with Flutter(五種視窗 2:23、Linux 範例 4:45、Window API 5:13)

同一個範例 app 在 Linux、macOS、Windows 開出對話框與衛星視窗

Flutter 可以一個 app 開多個原生視窗,所有視窗共用同一棵 widget tree,Riverpod / Bloc 不用改就能跨視窗共享狀態。這是 Canonical(Ubuntu)主導、Google review 的多年合作。stable 3.50.0 並未開放,只有 main channel 加 flutter config --enable-windowing 才能用;API 標為 @internal,放在 package:flutter/src/widgets/_window.dart,官方說明不要用於正式環境或 pub 套件,連 patch 版都可能有 breaking change。

官方 blog:Multi-Window Reference Application(macOS)

五種視窗類型

視窗階層:regular 底下掛 dialog、popup、satellite、tooltip

上:五種視窗類型。下:視窗可以組成樹,regular 底下掛 dialog、popup、satellite、tooltip。(影片截圖:Multi-window on desktop with Flutter)

類型Controller行為典型用途
RegularWindowController標題列、可縮放/最大化/全螢幕主視窗、分頁拆成新視窗
DialogDialogWindowControllerparent 不為 null 就是 modal;必須回答才關得掉確認、表單
PopupPopupWindowController貼著 anchor,點別處就關右鍵選單、autocomplete
TooltipTooltipWindowController同 popup 但永不取得焦點按鈕說明
SatelliteSatelliteWindowController跟著 parent 移動 / 縮放工具箱、調色盤

其他角色:Window(controller:, child:) 負責渲染(main 已統一成這一個 widget;3.50.0 tag 時還是 DialogWindow、PopupWindow 等各自的 widget)、WindowManager + WindowRegistry 管理多個視窗、WindowControllerDelegate 等 mixin 接 onWindowCloseRequested / onWindowDestroyed、WindowScope 查 contentSizeOf、isActivatedOf、isMaximizedOf 等。控制器參數是 size:,依內容定尺寸用 .shrinkWrap()。在行動裝置建立 controller 會丟例外。

程式依官方範例 examples/multiple_windows 與 main 的 _window.dart 改寫:

// flutter channel main && flutter config --enable-windowing
// ignore_for_file: invalid_use_of_internal_member, implementation_imports
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/src/widgets/_window.dart';

class MainWindowDelegate with WindowControllerDelegate {
  @override
  void onWindowDestroyed() {
    super.onWindowDestroyed();
    // 主視窗關掉就結束 app(不要用 dart:io 的 exit(0))
    ServicesBinding.instance.exitApplication(AppExitType.required);
  }
}

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  final mainWindow = WindowController(
    size: const Size(1000, 700),
    title: 'Admin',
    delegate: MainWindowDelegate(),
  );
  runWidget(WindowManager(initialWindows: [
    WindowEntry(
      controller: mainWindow,
      builder: (_) => const MaterialApp(home: HomePage()),
    ),
  ]));
}

// 在 HomePage 裡彈出「真正的」對話框視窗
void openConfirm(BuildContext context) {
  final registry = WindowRegistry.of(context);
  late final WindowEntry entry;
  final dialog = DialogWindowController(
    size: const Size(420, 300),
    parent: WindowScope.of(context), // 有 parent = modal
    title: '確認排班',
    delegate: _OnDestroyed(() => registry.unregister(entry)),
  );
  entry = WindowEntry(controller: dialog, builder: (_) => const ConfirmPage());
  registry.register(entry);
}

class _OnDestroyed with DialogWindowControllerDelegate {
  _OnDestroyed(this.onDone);
  final VoidCallback onDone;
  @override
  void onWindowDestroyed() {
    onDone();
    super.onWindowDestroyed();
  }
}
// 任何視窗內取尺寸:WindowScope.contentSizeOf(context)

版本演進(追蹤 issue #30701):#188866 把 WindowManager 移到 WidgetsApp 之上,次視窗可有自己的 Navigator;#189363 把 RegularWindow* 更名為 Window*、sizedToContent 改為 shrinkWrap;#196002(2026-11-10)把各類型 widget 合併成單一 Window。未來 Material 的 dialog、menu、tooltip 會在桌面上自動變成獨立視窗,多數人不必直接碰這層。

情境延伸

  • 後台 / 排班系統:主視窗是行事曆,點班別開 satellite 視窗顯示人員細節,拖主視窗時跟著走。
  • 客服工具:每個工單是獨立視窗,共用同一個 Riverpod provider,一邊改另一邊即時更新。
  • 現在只適合在 main channel 的分支做 PoC;等 stable 開放再進正式版。

延伸閱讀

設計系統:Mix、Remix、Widgetbook#

影片:The new way to build custom design libraries(Mix 2:07、Remix 4:13、Widgetbook 5:06、AI 視覺測試迴圈 6:25)

material_ui / cupertino_ui 拆出後,「自建設計系統」在 Flutter 更可行。影片介紹了三個社群工具(皆非 Google 官方套件):

工具版本(2026-11-12)定位
mix2.4.0用 Styler 描述樣式、變體(dark / hover / press)與動畫
remix0.2.0建在 Mix 上、無預設外觀的元件庫(Fortal 主題)
widgetbook3.25.0元件目錄、視覺回歸、無障礙檢查

同一張卡片:Flutter 原生 vs. Mix 寫法

Mix 2.4.0

import 'package:mix/mix.dart';

final accentCard = BoxStyler()
    .padding(EdgeInsetsMix.all(16))
    .color(Colors.white)
    .borderRadius(BorderRadiusMix.circular(12))
    .onDark(BoxStyler().color(Colors.grey.shade900))
    .onHovered(BoxStyler().color(Colors.green.shade50))
    .animate(AnimationConfig.easeOut(200.ms));

// 帶互動狀態的容器
accentCard.pressable(onPress: onTap, child: const Text('已報到'));

Mix:加一行就支援 dark mode、hover 陰影與過場動畫

每加一個變體(.onDark、.onHovered、.animate),右邊的方塊就多一種行為(影片 3:21 起,2.5 倍速)。

Remix 0.2.0

final primaryButton = RemixButtonStyle()
    .padding(EdgeInsetsMix.symmetric(horizontal: 16, vertical: 10))
    .backgroundColor(Colors.green)
    .foregroundColor(Colors.white)
    .borderRadius(BorderRadiusMix.circular(8))
    .onHovered(RemixButtonStyle().backgroundColor(Colors.green.shade700))
    .animate(AnimationConfig.spring(300.ms));

RemixButton(label: '報到', onPressed: () {}, style: primaryButton);
// 整體主題:用 FortalScope 包住 app

Widgetbook 的模式矩陣:主題、視窗尺寸、語系、字級

同一個元件與 Figma 稿在 Light × iPhone SE × EN × 100% 字級下逐一比對

Widgetbook 3.25.0(影片中的「modes」是 v4 beta 功能;穩定的 3.x 用 addons 達成同樣的矩陣)

// widgetbook/lib/main.dart
@widgetbook.App()
class WidgetbookApp extends StatelessWidget {
  const WidgetbookApp({super.key});
  @override
  Widget build(BuildContext context) => Widgetbook.material(
        directories: directories, // build_runner 產生
        addons: [
          MaterialThemeAddon(themes: [
            WidgetbookTheme(name: 'Light', data: ThemeData.light()),
            WidgetbookTheme(name: 'Dark', data: ThemeData.dark()),
          ]),
          ViewportAddon([IosViewports.iPhone13, AndroidViewports.samsungGalaxyS20]),
          TextScaleAddon(min: 1.0, max: 2.0),
        ],
      );
}

@widgetbook.UseCase(name: 'Default', type: CheckInCard)
Widget checkInCardDefault(BuildContext context) => const CheckInCard();
dart run build_runner build -d

Widgetbook Cloud 的無障礙檢查

無障礙檢查與視覺回歸比對屬於 Widgetbook Cloud(付費服務)。

情境延伸

  • 拆出 material_ui 後先盤點:哪些畫面真的需要 Material 元件,哪些已經是自訂樣式。
  • Widgetbook 的 Light / Dark × 兩種尺寸 × 大字體矩陣,可以抓到「大字體破版」這類 QA 常漏的問題。
  • Mix / Remix 仍在 0.x–2.x 快速迭代,先用在新的獨立元件,不要一次改寫整個 app。

延伸閱讀

Web:Wasm 三大瀏覽器全通、原子化部署#

影片:The power of Dart & Wasm(三瀏覽器 + SIMD 2:46、Wasm 走出瀏覽器 4:55、原子化部署 9:15)

DartPad preview:編譯與執行全部在瀏覽器內完成

Chrome、Safari、Firefox 上 Wasm 約快 2.2–2.5 倍

一次建置,自動 fallback:flutter_bootstrap.js 啟動時檢查瀏覽器是否支援 Wasm

更新重點怎麼用
三大瀏覽器 WasmChromium 137+、Safari 26.2+、Firefox 147+預設只有 Chromium 走 Wasm;其他用 wasmAllowList 開啟
Wasm SIMD硬體向量運算加速自動套用
WebParagraph(實驗)用瀏覽器原生 TextMetrics 排字,不必下載字型與 ICUpreferWebParagraph: true,只限 CanvasKit JS 建置
--web-content-hash檔名帶 8 字元 SHA-256,部署不再新舊混用canvaskit 不加 hash;與 deferred wasm loading 不相容
dart compile wasm --standalone純 Dart 程式編成 WASI 可執行檔配合 wasmtime
// web/flutter_bootstrap.js
{{flutter_js}}
{{flutter_build_config}}
_flutter.loader.load({
  config: {
    wasmAllowList: { blink: true, webkit: true, gecko: true }, // 另有 unknown
    // preferWebParagraph: true, // 實驗:Chrome 需 --enable-blink-features=ExtendedTextMetrics
  },
});
flutter build web --wasm --web-content-hash

官方 firebase.json 快取範例(index.html 不快取、帶 hash 的檔案永久快取)

{
  "hosting": {
    "public": "build/web",
    "headers": [
      { "source": "**", "headers": [{ "key": "Cache-Control", "value": "max-age=0, must-revalidate" }] },
      { "source": "assets/**", "headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }] },
      { "source": "main.dart.*.+(js|wasm|mjs)", "headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }] }
    ],
    "rewrites": [{ "source": "**", "destination": "/index.html" }]
  }
}

TextMetrics 帶來的好處

Wasm 走出瀏覽器

dart compile wasm --standalone bin/main.dart -o main.wasm
wasmtime run main.wasm

# Simon Binder 的 wasm_tools(0.1.1-preview.1,概念驗證):WASI component model
dart pub add dev:wasm_tools
dart run wasm_tools compile bin/app.dart
wasmtime run bin/app.wasm

WASI component model 與 wasm_tools

情境延伸

  • 後台 / 內部工具類 Flutter Web:開啟三瀏覽器 Wasm,用 flutter-wasm-compare 量測自己的畫面。
  • 部署後偶發「白畫面 / 舊版 JS」:加上 --web-content-hash 與上面的快取設定通常就能解決。
  • 共用的純 Dart 邏輯(計薪、驗證規則)可編成 standalone Wasm,放在 edge 或其他語言的服務裡重用。

延伸閱讀

GenUI / A2UI 1.0:讓 AI 回覆「介面」而不是一大段文字#

影片:What A2UI 1.0 means for Flutter devs(allowedParents/Children 3:02、Express 4:21、多 catalog 6:59、agentFunction 8:10)、Client side functions for Flutter GenUI

A2UI(Agent-to-UI)是 agent 用 catalog 裡的元件拼出 UI、由 Flutter 端渲染的協定。a2ui.org 目前把 v1.0 標為「Current」規格;Flutter 端套件已換代:genui 正式 deprecated,改由三個模組化套件接手,API 也不同(套件版號仍是 0.x)。影片的示範 app Commis 與官方 codelab 目前仍是舊的 genui 寫法,照抄前要注意。

套件版本(2026-11-12)用途
a2ui_core0.7.0MessageProcessor、Catalog、FunctionImplementation、AgentFunction
a2ui_flutter0.3.0把 surface 渲染成 Flutter widget
a2ui_agent0.3.0app 自己當 agent 時用(A2uiGenerator、Express)
json_schema_builder0.1.7定義 schema(a2ui_core 不會 re-export)
genui0.10.4舊版,已 deprecated

影片示範:agent 發現客人對甲殼類過敏,自己過濾掉含蝦的食譜,只給 7 個可勾選選項——沒有人寫過這個篩選畫面。

Commis:外燴業者的工作、食譜、食材畫面與 agent 分頁

影片示範 app Commis:下方分頁是一般 UI,最右邊是 agent 畫面(原始碼)。

1.0 新東西解決什麼關鍵細節
Client-side functionsLLM 算錢會錯、要等網路繼承 FunctionImplementation,在裝置上執行,結果確定且立即
allowedParents / allowedChildrenagent 把整張訂單表塞進按鈕裡編進 prompt,且 processor 會以 UNALLOWED_PARENT / UNALLOWED_CHILD 拒收,回報 agent 自我修正
Express 格式生成太慢、傳輸太大a2ui_agent 的預設格式(ExpressFormatFactory);Solid Software 的病歷儀表板約 10 秒 → 5 秒
多 catalogcatalog 綁死一個 surfaceMessageProcessor(catalogs: [...]) 一次註冊多個
AgentFunction改一個數字也要開新一輪 LLM元件呼叫 agent 端函式,結果回寫資料,不經模型
Metadata想帶 trace id、樣式旗標SurfaceModel.metadata / .extensions、node.metadata

0.9:按鈕裡塞進整張表

1.0:在渲染前攔下,讓 agent 自我修正

上:0.9 沒有巢狀限制,按鈕裡塞進整張表。下:1.0 在渲染前擋下、讓 agent 自我修正。

0.9:一個 surface 只能用一個 catalog

1.0:可同時組合 Core、Finance、Profile catalog

上:0.9 一個 surface 只能用一個 catalog。下:1.0 可同時組合 Core、Finance、Profile catalog。(影片截圖:What A2UI 1.0 means for Flutter devs)

Client-side function(a2ui_core 0.7.0)

dependencies:
  a2ui_flutter: ^0.3.0
  a2ui_core: ^0.7.0
  a2ui_agent: ^0.3.0          # 只有 app 自己組 prompt 時需要
  json_schema_builder: ^0.1.7
import 'package:a2ui_core/a2ui_core.dart';
import 'package:a2ui_flutter/a2ui_flutter.dart';
import 'package:json_schema_builder/json_schema_builder.dart' hide ValidationResult;

class CalculatePayFunction extends FunctionImplementation {
  CalculatePayFunction(this.rates)
      : super(
          name: 'calculatePay',
          description: 'Returns the pay for a shift as a TWD string, e.g. NT\$1,830.',
          returnType: A2uiReturnType.string,
          argumentSchema: S.object(
            properties: {
              'shift_id': S.string(description: 'The shift ID.'),
              'hours': S.number(description: 'Worked hours.'),
            },
            required: ['shift_id', 'hours'],
          ),
        );
  final PayRateService rates;

  @override
  Object? execute(Map<String, dynamic> args, DataContext context,
      [CancellationSignal? cancellationSignal]) {
    final hours = num.tryParse(args['hours'].toString())?.toDouble() ?? 0;
    final pay = rates.payFor(args['shift_id'].toString(), hours); // 純 Dart、可單元測試
    return 'NT\$${pay.toStringAsFixed(0)}';
  }
}

final payLabel = ComponentImplementation(
  name: 'PayLabel',
  schema: S.object(
      properties: {'text': CommonSchemasV1.dynamicString}, required: ['text']),
  allowedParents: ['ShiftCard'], // 只能放在 ShiftCard 裡
  allowedChildren: const [],     // 不能再包別的元件
  builder: (context, node, props, buildChild) => Text(props.string('text') ?? ''),
);

final shiftCatalog = CatalogImplementation(
  id: 'https://example.com/catalogs/shift.json', // 1.0 的 catalog id 用 URI
  protocolVersion: A2uiProtocolVersion.v1_0,
  components: [payLabel],
  functions: [CalculatePayFunction(PayRateService())],
);

// 一個 processor 可同時註冊多個 catalog
final processor = MessageProcessor<ComponentImplementation>(
  catalogs: [basicCatalog(), shiftCatalog],
  outboundListener: sendToAgent, // actions、errors、callAgentFunction 都從這裡出去
  unifiedOutbound: true,
);

// 讀取 agent 帶來的 metadata(例如 trace id)
processor.model.onSurfaceCreated.subscribe((surface) {
  final ext = surface.extensions;
});

allowedParents / allowedChildren 的 schema 示意

Agent 端(Dart server 或 app 自己當 agent):A2uiGenerator(catalogs: [...]) 預設就用 Express 格式;AgentFunction 放在 agent 的 CatalogApi(...).copyWith(agentFunctions: [...]),由 RendererToAgentMessageProcessor 回應。注意 CatalogImplementation 不能放 AgentFunction。

舊版對照:影片與 Commis 範例 中的 SynchronousClientFunction、executeSync、Catalog([...], functions:, catalogId:) 都是 genui 的 API,在 a2ui_* 套件中不存在。

決定「哪個動作要叫模型」(Somnio 在金融 demo 的做法):是「問題」就開新一輪;答案已知的操作(改金額重算)走 AgentFunction 或 client function。模型不做算術。他們測了 8 個金融場景,5 個用一般表單就夠,只有 3 個真的值得用 GenUI。(該 repo 的 v1 前端用的是自製 renderer,不是 a2ui_flutter。)

情境延伸

  • 薪資 / 時薪試算:「這班做 6 小時可以拿多少?」→ agent 決定畫面,上面的 calculatePay 算錢,絕不讓 LLM 寫數字。
  • 找工作助理:用 allowedChildren 限制 job card 只能放標題、地點、時薪、按鈕,保住品牌一致性。
  • 多團隊分工:核心 UI 維護基礎 catalog,各業務團隊各自發佈 domain catalog。
  • 可觀測性:用 metadata 帶 trace id,把後端 agent log 與 app 的 Datadog / Sentry 事件串起來。
  • 先問「一般表單能不能解決?」——大多數時候可以。

延伸閱讀

裝置端 AI:LiteRT-LM + Gemma 4、MediaPipe、Genkit Dart 1.0#

影片:On-device AI with LiteRT-LM and Gemma(選 Gemma 模型 4:19、Flutter 實作 5:31、Tool calling 7:18)、Google AI Edge 10 天做出桌面 AI app

LiteRT-LM 讓 Flutter 跨平台部署 LLM(iOS、Android、Web、Windows、macOS、Linux、IoT)

Gemma 4 家族:2B / 4B 給行動與邊緣裝置,12B / 26B / 31B 給筆電、桌機與雲端

先弄清楚誰是官方:

東西來源版本(2026-11-12)
LiteRT-LM 執行引擎、Gemma 4 模型Google 官方—
flutter_edge_ai + flutter_edge_ai_litertlm社群套件(sashadenisov.dev,前身 flutter_gemma),影片示範用它2.6.0 / 1.20.0
MediaPipe for FlutterGoogle 官方,5 個 task 套件 + mediapipe_core,22 個 task 類別0.2.1(影片畫面是 0.1.3)
Genkit DartGoogle 官方,1.0 發布文1.0.2

LiteRT-LM + Gemma 4(flutter_edge_ai 寫法)

dependencies:
  flutter_edge_ai: ^2.6.0
  flutter_edge_ai_litertlm: ^1.20.0
await FlutterEdgeAi.initialize(inferenceEngines: const [LiteRtLmEngine()]);

// Gemma 4 E2B 約 2.59 GB、E4B 約 3.66 GB,首次需下載
await FlutterEdgeAi.installModelFromCatalog(LiteRtLmModels.gemma4E2bIt.generic)
    .withProgress((p) => debugPrint('downloading $p%'))
    .install();

final model = await FlutterEdgeAi.getActiveModel(
  maxTokens: 2048,
  preferredBackend: PreferredBackend.gpu,
);

const weather = Tool(
  name: 'get_weather',
  description: 'Current weather for a city',
  parameters: {
    'type': 'object',
    'properties': {'city': {'type': 'string'}},
    'required': ['city'],
  },
);

final chat = await model.openChat(tools: const [weather]);
await chat.addQueryChunk(Message.text(text: '台北現在天氣?', isUser: true));

await for (final r in chat.generateChatResponseAsync()) {
  if (r is TextResponse) debugPrint(r.token);
  if (r is FunctionCallResponse) {
    final result = await weatherService.lookup(r.args['city']);
    await chat.addQuery(Message.toolResponse(
        toolName: r.name, response: result, toolCallId: r.id));
  }
}

平台需求:iOS 15+(static frameworks)、Android minSdk 30 且僅 arm64、macOS 14+。Hugging Face litert-community 另有 12B、26B-A4B、31B 等較大模型。

MediaPipe for Flutter 套件

MediaPipe(0.2.1,需 Flutter ≥ 3.47):vision / text / audio / decision / retrieval 五個 task 套件。

final classifier = await TextClassifier.create(
  TextClassifierOptions(model: TextModels.bertClassifier, maxResults: 2),
);

Genkit Dart 1.0(雲端 / server 端的 AI flow)

官方 blog:Cymbal Bistro 用 Genkit Dart + A2UI 串流訂位卡片

final ai = Genkit(plugins: []);
final hello = ai.defineFlow(
  name: 'hello',
  inputSchema: .string(defaultValue: 'World'),
  outputSchema: .string(),
  fn: (String subject, _) async => 'hello $subject',
);

情境延伸

  • 離線 / 隱私場景:班表摘要、工作說明翻譯、表單內容檢查可以在手機上跑,個資不出裝置。
  • 2.59 GB 的下載是主要成本:只在 Wi-Fi、使用者主動開啟時下載,且要有雲端 fallback。
  • 低階 Android(minSdk < 30)與 32 位元裝置不支援,上線前先看使用者裝置分佈。
  • 小模型(MediaPipe 文字分類、嵌入)先導入,比 LLM 小很多、效果穩定。

延伸閱讀

AI 開發工具:Dart / Flutter MCP、skills、agent plugins#

影片:The Future of Flutter’s Dev Experience(MCP 工具與 skills 在做什麼 15:18、團隊如何做 evals 21:35)

重點:模型會變強,但 analyzer、DevTools、VM service 這類「確定性」工具更重要——MCP 把它們交給 agent,skills 告訴 agent 怎麼寫得道地。

// 任何支援 MCP 的 agent(需 Dart 3.9+)
{ "mcpServers": { "dart": { "command": "dart", "args": ["mcp-server"] } } }
# Claude Code:官方 agent plugin(含 MCP + skills)
claude plugin marketplace add flutter/agent-plugins
claude plugin install dart-flutter@dart-flutter

# 其他 agent:只裝 skills
npx skills add flutter/agent-plugins --skill '*' --agent universal --yes

# 套件作者:取用 / 檢查套件自帶的 skills
dart run skills@ get
dart pub add dev:skills_lint

情境延伸

  • 團隊的 Claude Code 裝上 dart-flutter plugin,讓 agent 能跑 analyzer、看 widget tree、hot reload,少猜。
  • 內部共用套件可以自帶 skills,並用 skills_lint 在 CI 檢查。

延伸閱讀

LG webOS:Flutter 上電視#

影片:Build a webOS app with Flutter(安裝 SDK 4:47、建立、執行、hot reload 5:52)

  • SDK 由 LG 維護,是 Flutter 「pluggable platforms」的第一個外部平台。
  • 開發機只支援 Linux(macOS / Windows 用 WSL2 或 DevContainer);目標為 webOS TV 26+,輸出 .ipk,已有 25 個常用 plugin。
flutter-webos doctor -v
flutter-webos precache
flutter-webos config --enable-custom-devices
flutter-webos custom-devices add          # 加入開發者模式的電視
flutter-webos devices
flutter-webos create --platforms webos my_tv_app
flutter-webos run -d <device-id>
flutter-webos build webos --release

flutter-webos create / run 後直接在 LG 電視上執行

情境延伸:店內 / 活動現場的大螢幕(排隊叫號、報到看板)可直接重用既有 Flutter 元件;注意遙控器的 focus 導覽要另外設計。

延伸閱讀

來源#