← Writing
Flutter · · 16 min read · 閱讀中文版

Flutter 3.50 & Dart 3.14 — Highlights

This round, Flutter 3.50 + Dart 3.14 move Material/Cupertino out of the framework, declare native interop stable, and add official iOS 27 support, while A2UI 1.0, on-device AI, and desktop multi-window arrive alongside. Below are the 11 videos on the @flutterdev channel from "Client side functions for Flutter GenUI" onward, plus details from the official blogs.

At a glance#

Video (published)Key pointWho should watch
The Future of Flutter’s Dev Experience (11/11)MCP provides tools, skills provide recipes, evals check whether AI-written Flutter is idiomaticAnyone writing code with AI
The power of Dart & Wasm (11/11)Wasm opt-in on Safari / Firefox, SIMD speedups, atomic deploys with --web-content-hashFlutter Web
Build a webOS app with Flutter (11/11)Official SDK for LG TVs, hot reload on the TV, the pluggable platforms planNew platforms, embedded
What A2UI 1.0 means for Flutter devs (11/11)Nesting guardrails, Express format (10 s → 5 s), multiple catalogs, agentFunctionAI assistants / GenUI
On-device AI with LiteRT-LM and Gemma (11/11)Offline, private, zero-token-cost local LLM, with chat and tool callingTeams that want AI features but worry about cost/privacy
Google AI Edge: local AI desktop app in 10 days (11/11)Eloquent went from iOS to desktop in 10 days; new apps reuse 60% of the foundation and save 80% of the timePMs, tech leads
Flutter interop with Pigeon FFI (11/11)useJni: true: keep Pigeon’s typed contract but call directly over JNI, no serializationPlugin / native bridge authors
Custom design libraries with Flutter (11/11)Raw widgets + Mix/Remix styling layer + Widgetbook visual regressionDesign system, UI team
Multi-window on desktop (11/11)Experimental multi-window API, five window types, one widget tree with shared stateDesktop apps
What’s new in Flutter 3.50 and Dart 3.14 (11/11)Release overview: dart migrate, material_ui, iOS 27, SwiftPM, hybrid compositionEveryone
Client side functions for Flutter GenUI (11/05)Let the LLM hand work like price calculation to local Dart functions: verifiable results, no latencyAI assistants / GenUI

Dart 3.14: no new syntax, but upgrades are now automatic#

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

Dart 3.14 adds no language features (this promotion and static extensions are still in progress); the focus is tooling and performance.

1. dart migrate: bump the SDK lower bound automatically + apply new idioms

Each run raises the pubspec SDK lower bound by one minor, in three steps: prepare (run the dart fixes needed before the bump) → bump → cleanup (optional). Requires the package to already be on Dart 3.0+.

dart migrate --dry-run                       # preview what will change
dart migrate --apply                         # do it all at once
dart migrate --apply --step=prepare          # or step by step, to split PRs
dart migrate --apply --step=bump,cleanup
dart migrate --apply --target-sdk=3.14.0

Official example (before → after), showing the new empty-body syntax and parameter final being removed:

// 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. Faster Analysis Server: file discovery (Find References), startup and restart, and documentColor are all faster, and formatting stalls less. In a 16-context DevTools workspace, a cold-start rename dropped from 2.0 s to 0.9 s (about 55% faster). Both your IDE and your AI agent benefit.

3. Pub uses ETags: get / upgrade / add send If-None-Match; a project with 144 dependencies saves about 1.5 MB of downloads, which CI will notice.

4. DartPad beta runs entirely in the browser: the compiler, analyzer, and pub are compiled to Wasm and run in a Web Worker, with multi-file support, pub.dev package imports, and full LSP, with no network round trips.

5. Other changes worth knowing

  • New API rangeEquals (integer typed data and ByteData), compiled straight to memcmp on the VM:
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, and package:js are formally deprecated and scheduled for removal; use dart:js_interop + package:web instead.
  • dart:mirrors is deprecated; new: dart run -e (run Dart in one line), dart pub upgrade pkg@latest, and the lint future_sync_value.
  • SIMD types (Int32x4, Float32x4, …) compile to v128 on Wasm, up to 4x faster in benchmarks.

In practice: in a multi-package monorepo (app + shared domain + design system), run dart migrate --step=prepare per package as its own small PR, then bump everything together; that makes review much lighter. The --dry-run output also works well as a change list to hand to an AI agent.

Further reading

Flutter 3.50 framework: Material / Cupertino officially move out of the SDK#

Video: 3.50 overview: Framework updates 1:59

The biggest change: package:flutter/material.dart and cupertino.dart are officially deprecated in favor of the standalone packages material_ui and cupertino_ui (weekly releases, semver). flutter create already imports material_ui by default. Timeline: 3.44 froze the in-SDK versions (= package 1.0.0) → 3.47 opt-in → 3.50 deprecated.

Official blog: migrating from bundled material.dart / cupertino.dart to material_ui / cupertino_ui

Migration steps

  1. dart fix --apply --code=migrate_design_widgets (adds dependencies, rewrites imports)
  2. If the pubspec was not updated: flutter pub add material_ui cupertino_ui
  3. Change imports to package:material_ui/material_ui.dart / package:cupertino_ui/cupertino_ui.dart
  4. Run dart fix again (import ordering, etc.)

What if third-party packages still use the old imports? Use MaterialUiCompatibilityBridge (CupertinoUiCompatibilityBridge for Cupertino) to convert the new theme into the old one and inject localizations. It is itself marked @Deprecated: it is a transition tool only, to remove once your dependencies have migrated. It cannot fix type mismatches (for example, passing the SDK’s ColorScheme to a material_ui API):

import 'package:material_ui/material_ui.dart';

MaterialApp(
  theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2E7D32))),
  // Wrap the whole app once; or wrap only the subtree that uses old packages
  // ignore: deprecated_member_use
  builder: (context, child) => MaterialUiCompatibilityBridge(child: child!),
  home: const HomeScreen(),
);

Note for package authors: migrating is a breaking change for your users, so release a major version.

New widgets and APIs (the practical ones)

CategoryWhat’s newWhere to use it
SliverSliverClipRect, SliverClipRRect (clipOverlap)Rounded clipping for card-style lists, stacking with a pinned header
ListsitemExtentBuilder on ListView.separatedFixed-extent optimization while keeping separators
ImagesDecorationImage.placeholder, FadeInImageTransition.fadeInOver, ResizeImage.useLogicalSizePlaceholders before avatars / merchant logos load
InputAutofillHints.emailOTPCode, numeric keyboard password (Android), HyphensEmail OTP autofill, PIN codes
AccessibilitySemanticsRole.slider, MediaQueryData.reduceMotion, Esc closes TooltipRespect the system “reduce motion” setting
NavigationNavigator.popUntilWithResult, showDialog(barrierBuilder:)Return to the start of a multi-step flow with a result

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

Breaking changes: ignoringSemantics removed from IgnorePointer / AbsorbPointer; soft hyphens show ”-” at line breaks; WidgetSpan in RTL text follows natural reading order; on the web with viewport-fit=cover, SafeArea accounts for the notch.

In practice

  • popUntilWithResult: when the application flow (pick a shift → accept terms → upload ID) finishes, go straight back to the job list carrying “applied”, instead of relying on a global event to tell the list to refresh.
  • itemExtentBuilder: jumping to item N in a long list (for example “today”) no longer requires building every item before it.
  • reduceMotion: turn off Lottie / transition animations when the system has reduce motion on.

Further reading

Platform updates: iOS 27, Android secure defaults, Engine#

Video: iOS 3:05, Android 4:30, Engine 5:17

iOS / macOS

  • 3.50 is the first stable after Xcode 27 / iOS 27; upgrade if you need to support iOS 27 users. Newly paired devices are set up automatically; flutter screenshot works on physical devices again (so an AI agent can see before/after screens itself). The new iPhone duo foldable form factor is also supported.
  • Touch latency: 16 ms lower on 60 Hz devices and 8 ms lower on 120 Hz ProMotion, tested back to iPhone 6s.
  • CocoaPods trunk becomes read-only in December 2026. Projects with Swift Package Manager turned off now get a warning on every CLI command.
  • Building on Intel Macs shows a deprecation warning and will be removed in a future release; you can use flutter config --enable-macos-arm64-only.

Official blog: iOS touch scheduling cuts a full frame of latency

Android

  • Release-mode Intent hardening: the route, dart_entrypoint, and cached_engine_id extras are ignored, and engine flags passed via Intent or --use-application-binary (such as --dart-flags) are rejected. Debug builds only warn. If you pass the initial route for deep links via an Intent extra, move to go_router / app links.
  • Hybrid Composition++: hands platform view compositing to the Android OS, fixing scroll tearing and text input issues. Requires Android 14 (API 34)+ with Impeller on Vulkan, and falls back automatically otherwise. On by default in beta; on stable enable it manually with flutter config --enable-hcpp, and it is active when the log shows Using HCPP platform view rendering strategy. Test apps heavy on maps, WebViews, and ads first.
  • Built-in Kotlin: plugins that apply the Kotlin Gradle Plugin themselves will soon fail to compile; switch to built-in Kotlin and declare flutter: ">=3.44.0".
  • New project defaults: 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 rendering (desktop) supports lines, thin borders, rounded corners, and gradients; macOS gradients and repeated text shadows are faster.
  • Custom embedders: Vulkan external textures, FlutterEngineNotifyLowMemoryWarning (clears Impeller’s offscreen caches on low memory).
  • Flutter GPU supports 2D texture arrays, with clearer shader pipeline error messages.
  • Windows now requires OpenGL ES 3 by default and supports non-ASCII paths (Chinese folder names no longer break things).

Official blog: Impeller SDF vs. MSAA, zoomed pixels

Further reading

Native interop: FFIgen / JNIgen stable, Pigeon direct calls (preview)#

Video: Flutter interop with Pigeon FFI (comparing the three approaches 1:05, defining @HostApi 2:00, permission flow on a device 4:05), 3.50 overview: Native interop 1:13

FFIgen and JNIgen reach stable

  • FFIgen / JNIgen are officially stable: generate Dart bindings directly from C / Objective-C / Swift headers and Java / Kotlin classes, with synchronous calls and no platform channel.
  • Pigeon 30’s native interop mode (preview): one Pigeon definition, with Android going through JNI (useJni: true) and iOS / macOS through FFI (useFfi: true), no more serializing to a MethodChannel. It does not support Windows / Linux yet, and the team explicitly says not to use it in production.

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

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

Official blog: MethodChannel serialization vs. direct JNI / FFI calls

Pigeon definition (pigeon 30.0.2, checked against the official 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
# Set ANDROID_HOME first, and run ./gradlew compileReleaseKotlin once in example/android
dart run pigeon --input pigeons/messages.dart

Output: messages.g.dart, messages.native_interop.g.dart, plus messages.g.jni.dart / messages.g.ffi.dart.

// Dart side: use the native interop constructor
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 sample app: check and request camera permission

The sample app from the video: the button calls Kotlin directly over JNI, and the status updates immediately after the system permission dialog.

In practice

  • Frequent small calls (permission checks, device info, battery / network status) benefit most: no serialization and no thread hop.
  • Keep large SDKs (push, payments, maps) on their existing plugins for now; reassess once interop mode leaves preview.
  • For plugins the team writes itself, try useJni / useFfi in the example app first, measure cold start and call latency, then decide whether to migrate.

Further reading

Desktop multi-window API (experimental, main channel only)#

Video: Multi-window on desktop with Flutter (five window types 2:23, Linux example 4:45, Window API 5:13)

The same sample app opening dialog and satellite windows on Linux, macOS, and Windows

One Flutter app can open multiple native windows, and all windows share one widget tree, so Riverpod / Bloc share state across windows unchanged. This is a multi-year collaboration led by Canonical (Ubuntu) and reviewed by Google. It is not available in stable 3.50.0; it only works on the main channel with flutter config --enable-windowing. The API is marked @internal and lives in package:flutter/src/widgets/_window.dart; the team says not to use it in production or in pub packages, and even patch releases may contain breaking changes.

Official blog: Multi-Window Reference Application on macOS

五種視窗類型

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

Top: the five window types. Bottom: windows can form a tree, with dialog, popup, satellite, and tooltip windows under a regular window. (Video screenshot: Multi-window on desktop with Flutter)

TypeControllerBehaviorTypical use
RegularWindowControllerTitle bar, resizable/maximizable/full screenMain window, a tab split into a new window
DialogDialogWindowControllerModal when parent is non-null; must be answered before it can closeConfirmations, forms
PopupPopupWindowControllerAttached to an anchor, closes when you click elsewhereContext menus, autocomplete
TooltipTooltipWindowControllerLike popup but never takes focusButton hints
SatelliteSatelliteWindowControllerMoves / resizes with its parentToolboxes, color palettes

Other roles: Window(controller:, child:) handles rendering (main has unified this into a single widget; at the 3.50.0 tag there were still separate widgets such as DialogWindow and PopupWindow), WindowManager + WindowRegistry manage multiple windows, mixins such as WindowControllerDelegate hook onWindowCloseRequested / onWindowDestroyed, and WindowScope queries contentSizeOf, isActivatedOf, isMaximizedOf, and so on. The controller parameter is size:; use .shrinkWrap() to size to content. Creating a controller on a mobile device throws.

The code is adapted from the official example examples/multiple_windows and _window.dart on main:

// 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();
    // Quit the app when the main window closes (do not use exit(0) from dart:io)
    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()),
    ),
  ]));
}

// Open a "real" dialog window from 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), // has a parent = modal
    title: 'Confirm schedule',
    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();
  }
}
// Get the size inside any window: WindowScope.contentSizeOf(context)

How it evolved (tracking issue #30701): #188866 moved WindowManager above WidgetsApp so secondary windows can have their own Navigator; #189363 renamed RegularWindow* to Window* and sizedToContent to shrinkWrap; #196002 (2026-11-10) merged the per-type widgets into a single Window. In the future, Material dialogs, menus, and tooltips will automatically become separate windows on desktop, so most people will not need to touch this layer directly.

In practice

  • Admin / scheduling system: the main window is a calendar; clicking a shift opens a satellite window with staff details that follows the main window when it is dragged.
  • Support tool: each ticket is its own window, sharing one Riverpod provider, so an edit in one updates the other immediately.
  • For now this only suits a PoC on a main-channel branch; wait for stable before shipping.

Further reading

Design systems: Mix, Remix, Widgetbook#

Video: The new way to build custom design libraries (Mix 2:07, Remix 4:13, Widgetbook 5:06, AI visual testing loop 6:25)

With material_ui / cupertino_ui split out, building your own design system in Flutter becomes more practical. The video covers three community tools (none are official Google packages):

ToolVersion (2026-11-12)Role
mix2.4.0Describe styles, variants (dark / hover / press), and animation with Stylers
remix0.2.0Unstyled component library built on Mix (Fortal theme)
widgetbook3.25.0Component catalog, visual regression, accessibility checks

The same card: plain 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));

// Container with interaction states
accentCard.pressable(onPress: onTap, child: const Text('Checked in'));

Mix: one line each adds dark mode, a hover shadow, and a transition

Each variant you add (.onDark, .onHovered, .animate) gives the box on the right one more behavior (from 3:21 in the video, at 2.5x speed).

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: 'Check in', onPressed: () {}, style: primaryButton);
// App-wide theme: wrap the app in FortalScope

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

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

Widgetbook 3.25.0 (the “modes” in the video are a v4 beta feature; stable 3.x builds the same matrix with addons)

// widgetbook/lib/main.dart
@widgetbook.App()
class WidgetbookApp extends StatelessWidget {
  const WidgetbookApp({super.key});
  @override
  Widget build(BuildContext context) => Widgetbook.material(
        directories: directories, // generated by 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

Accessibility checks in Widgetbook Cloud

Accessibility checks and visual regression comparison are part of Widgetbook Cloud (a paid service).

In practice

  • Once material_ui is split out, take stock first: which screens really need Material components, and which already use custom styling.
  • Widgetbook’s Light / Dark × two sizes × large text matrix catches problems QA often misses, such as layouts breaking at large font sizes.
  • Mix / Remix are still iterating fast at 0.x–2.x; use them on new standalone components first rather than rewriting the whole app at once.

Further reading

Web: Wasm on all three major browsers, atomic deploys#

Video: The power of Dart & Wasm (three browsers + SIMD 2:46, Wasm beyond the browser 4:55, atomic deploys 9:15)

DartPad preview: compiling and running entirely in the browser

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

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

UpdateKey pointHow to use it
Wasm on the three major browsersChromium 137+, Safari 26.2+, Firefox 147+Only Chromium uses Wasm by default; enable the others with wasmAllowList
Wasm SIMDHardware vector accelerationApplied automatically
WebParagraph (experimental)Lays out text with the browser’s native TextMetrics, no font or ICU downloadpreferWebParagraph: true, CanvasKit JS builds only
--web-content-hashFile names carry an 8-character SHA-256, so deploys no longer mix old and new filescanvaskit is not hashed; incompatible with deferred wasm loading
dart compile wasm --standaloneCompiles pure Dart programs to WASI executablesUse with wasmtime
// web/flutter_bootstrap.js
{{flutter_js}}
{{flutter_build_config}}
_flutter.loader.load({
  config: {
    wasmAllowList: { blink: true, webkit: true, gecko: true }, // there is also unknown
    // preferWebParagraph: true, // experimental: Chrome needs --enable-blink-features=ExtendedTextMetrics
  },
});
flutter build web --wasm --web-content-hash

Official firebase.json caching example (index.html is not cached; hashed files are cached forever)

{
  "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" }]
  }
}

What TextMetrics brings

Wasm beyond the browser

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

# Simon Binder's wasm_tools (0.1.1-preview.1, proof of concept): 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 and wasm_tools

In practice

  • Admin / internal-tool Flutter Web apps: enable Wasm on all three browsers and measure your own screens with flutter-wasm-compare.
  • Occasional “white screen / stale JS” after a deploy: adding --web-content-hash and the caching config above usually fixes it.
  • Shared pure-Dart logic (pay calculation, validation rules) can be compiled to standalone Wasm and reused at the edge or in services written in other languages.

Further reading

GenUI / A2UI 1.0: let AI reply with an interface instead of a wall of text#

Video: What A2UI 1.0 means for Flutter devs (allowedParents/Children 3:02, Express 4:21, multiple catalogs 6:59, agentFunction 8:10), Client side functions for Flutter GenUI

A2UI (Agent-to-UI) is a protocol in which an agent assembles UI from components in a catalog and the Flutter side renders it. a2ui.org currently marks v1.0 as the “Current” spec; the Flutter packages have been replaced: genui is formally deprecated, succeeded by three modular packages with a different API (package versions are still 0.x). The video’s demo app Commis and the official codelab still use the old genui API, so be careful before copying them.

PackageVersion (2026-11-12)Purpose
a2ui_core0.7.0MessageProcessor, Catalog, FunctionImplementation, AgentFunction
a2ui_flutter0.3.0Renders surfaces as Flutter widgets
a2ui_agent0.3.0For when the app itself acts as the agent (A2uiGenerator, Express)
json_schema_builder0.1.7Defines schemas (a2ui_core does not re-export it)
genui0.10.4Old package, deprecated

Video demo: the agent learns that the guest is allergic to shellfish, filters out recipes containing shrimp on its own, and offers only 7 selectable options. Nobody wrote that filter screen.

Commis: a caterer's jobs, recipes, ingredients screens and an agent tab

The video’s demo app Commis: the bottom tabs are regular UI, and the rightmost is the agent screen (source).

New in 1.0Problem it solvesKey details
Client-side functionsLLMs get money math wrong and you wait on the networkExtend FunctionImplementation; runs on the device, results are deterministic and immediate
allowedParents / allowedChildrenThe agent stuffs a whole order table into a buttonCompiled into the prompt, and the processor rejects with UNALLOWED_PARENT / UNALLOWED_CHILD, reporting back so the agent corrects itself
Express formatGeneration too slow, payload too largeDefault format in a2ui_agent (ExpressFormatFactory); Solid Software’s medical records dashboard went from about 10 s → 5 s
Multiple catalogsA catalog is tied to one surfaceRegister several at once with MessageProcessor(catalogs: [...])
AgentFunctionChanging one number needs a new LLM turnA component calls an agent-side function and the result is written back to the data, bypassing the model
MetadataWant to pass a trace id or style flagsSurfaceModel.metadata / .extensions, node.metadata

0.9:按鈕裡塞進整張表

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

Top: 0.9 has no nesting limits, so a whole table ends up inside a button. Bottom: 1.0 blocks it before rendering and lets the agent correct itself.

0.9:一個 surface 只能用一個 catalog

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

Top: in 0.9 a surface can use only one catalog. Bottom: 1.0 can combine Core, Finance, and Profile catalogs at once. (Video screenshot: 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          # only needed if the app builds the prompt itself
  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); // pure Dart, unit-testable
    return 'NT\$${pay.toStringAsFixed(0)}';
  }
}

final payLabel = ComponentImplementation(
  name: 'PayLabel',
  schema: S.object(
      properties: {'text': CommonSchemasV1.dynamicString}, required: ['text']),
  allowedParents: ['ShiftCard'], // may only be placed inside ShiftCard
  allowedChildren: const [],     // may not wrap other components
  builder: (context, node, props, buildChild) => Text(props.string('text') ?? ''),
);

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

// One processor can register several catalogs at once
final processor = MessageProcessor<ComponentImplementation>(
  catalogs: [basicCatalog(), shiftCatalog],
  outboundListener: sendToAgent, // actions, errors, and callAgentFunction all go out here
  unifiedOutbound: true,
);

// Read metadata sent by the agent (for example a trace id)
processor.model.onSurfaceCreated.subscribe((surface) {
  final ext = surface.extensions;
});

Schema sketch for allowedParents / allowedChildren

Agent side (a Dart server, or the app acting as its own agent): A2uiGenerator(catalogs: [...]) uses the Express format by default; AgentFunctions go in the agent’s CatalogApi(...).copyWith(agentFunctions: [...]) and are answered by RendererToAgentMessageProcessor. Note that CatalogImplementation cannot hold AgentFunctions.

Old API mapping: SynchronousClientFunction, executeSync, and Catalog([...], functions:, catalogId:) in the video and the Commis example are genui APIs and do not exist in the a2ui_* packages.

Deciding which actions call the model (Somnio’s approach in their finance demo): a “question” starts a new turn; operations whose answer is already known (recalculating after changing an amount) go through an AgentFunction or a client function. The model does no arithmetic. They tested 8 finance scenarios: 5 were fine with a regular form, and only 3 were really worth GenUI. (That repo’s v1 frontend uses a custom renderer, not a2ui_flutter.)

In practice

  • Pay / hourly-rate estimates: “How much do I get for 6 hours on this shift?” → the agent decides the screen, the calculatePay above does the math, and the LLM never writes the number.
  • Job search assistant: use allowedChildren to restrict a job card to title, location, hourly rate, and button, keeping the brand consistent.
  • Multi-team ownership: the core UI team maintains the base catalog, while each business team publishes their own domain catalog.
  • Observability: pass a trace id in metadata to connect backend agent logs with the app’s Datadog / Sentry events.
  • Ask first: “Can a regular form solve this?” Most of the time it can.

Further reading

On-device AI: LiteRT-LM + Gemma 4, MediaPipe, Genkit Dart 1.0#

Video: On-device AI with LiteRT-LM and Gemma (choosing a Gemma model 4:19, Flutter implementation 5:31, Tool calling 7:18), Google AI Edge: a desktop AI app in 10 days

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

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

First, be clear about what is official:

ItemSourceVersion (2026-11-12)
LiteRT-LM runtime, Gemma 4 modelsOfficial Google—
flutter_edge_ai + flutter_edge_ai_litertlmCommunity packages (sashadenisov.dev, formerly flutter_gemma); the video demo uses them2.6.0 / 1.20.0
MediaPipe for FlutterOfficial Google: 5 task packages + mediapipe_core, 22 task classes0.2.1 (the video shows 0.1.3)
Genkit DartOfficial Google, 1.0 announcement1.0.2

LiteRT-LM + Gemma 4 (with 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 is about 2.59 GB and E4B about 3.66 GB; downloaded on first use
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: "What's the weather in Taipei right now?", 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));
  }
}

Platform requirements: iOS 15+ (static frameworks), Android minSdk 30 and arm64 only, macOS 14+. Hugging Face litert-community also hosts larger models such as 12B, 26B-A4B, and 31B.

MediaPipe for Flutter packages

MediaPipe (0.2.1, requires Flutter ≥ 3.47): five task packages: vision / text / audio / decision / retrieval.

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

Genkit Dart 1.0 (AI flows in the cloud / on the server)

Official blog: Cymbal Bistro streams an A2UI reservation card with Genkit Dart

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

In practice

  • Offline / privacy scenarios: schedule summaries, job description translation, and form content checks can run on the phone, so personal data never leaves the device.
  • The 2.59 GB download is the main cost: download only on Wi-Fi when the user opts in, and keep a cloud fallback.
  • Low-end Android (minSdk < 30) and 32-bit devices are not supported; check the user device distribution before launch.
  • Start with small models (MediaPipe text classification, embeddings): much smaller than an LLM, with stable results.

Further reading

AI developer tools: Dart / Flutter MCP, skills, agent plugins#

Video: The Future of Flutter’s Dev Experience (what MCP tools and skills do 15:18, how the team runs evals 21:35)

Key point: models will keep getting better, but deterministic tools such as the analyzer, DevTools, and the VM service matter more. MCP hands them to the agent, and skills tell the agent how to write idiomatic code.

// Any MCP-capable agent (requires Dart 3.9+)
{ "mcpServers": { "dart": { "command": "dart", "args": ["mcp-server"] } } }
# Claude Code: official agent plugin (MCP + skills)
claude plugin marketplace add flutter/agent-plugins
claude plugin install dart-flutter@dart-flutter

# Other agents: skills only
npx skills add flutter/agent-plugins --skill '*' --agent universal --yes

# Package authors: fetch / lint the skills bundled with packages
dart run skills@ get
dart pub add dev:skills_lint

In practice

  • Install the dart-flutter plugin in the team’s Claude Code so the agent can run the analyzer, inspect the widget tree, and hot reload instead of guessing.
  • Internal shared packages can ship their own skills and check them in CI with skills_lint.

Further reading

LG webOS: Flutter on the TV#

Video: Build a webOS app with Flutter (installing the SDK 4:47, create, run, hot reload 5:52)

  • The SDK is maintained by LG and is the first external platform under Flutter’s “pluggable platforms”.
  • The development machine must be Linux (use WSL2 or a DevContainer on macOS / Windows); it targets webOS TV 26+, outputs .ipk, and already has 25 common plugins.
flutter-webos doctor -v
flutter-webos precache
flutter-webos config --enable-custom-devices
flutter-webos custom-devices add          # add a TV in developer mode
flutter-webos devices
flutter-webos create --platforms webos my_tv_app
flutter-webos run -d <device-id>
flutter-webos build webos --release

Running directly on an LG TV after flutter-webos create / run

In practice: large screens in stores or at events (queue calling, check-in boards) can reuse existing Flutter components directly; remote-control focus navigation needs to be designed separately.

Further reading

Sources#