
Why we built a pure-Dart, 0ms synchronous Generative UI state machine on top of Google's official A2UI protocol—supporting two-way reactive forms, Riverpod/BLoC interop, Flutter Material 3, and ANSI terminal TUIs.
"An asynchronous network stream belongs at the perimeter of your architecture; once an AI agent's UI lands on the client, every keystroke, validation rule, and leaf rebuild should execute in 0ms on a synchronous signal graph." — Randal L. Schwartz
When you see a new Generative UI announcement for Dart and Flutter, your first reaction is probably healthy skepticism: Do we really need another GenUI wrapper?
Most Generative UI prototypes stop at the initial parlor trick: an LLM streams a JSON payload over Server-Sent Events (SSE), a widget tree appears on screen, and everyone claps. The architectural cracks appear five seconds later when a user tries to interact with that generated interface:
StreamBuilder, async* generators, or asynchronous controller notifications. Every keystroke in a generated text field, every cross-field validation check, and every widget test pays a microtask and frame-delay tax.package:flutter/widgets.dart (BuildContext, Widget, ChangeNotifier), making it impossible to run or test the agent state machine in pure Dart, CLI tools, or terminal interfaces—and forcing you to adopt a bespoke state management paradigm that clashes with your existing Riverpod or flutter_bloc app.Today, we are publishing v1.0.0 of bloc_signals_genui and bloc_signals_genui_flutter on pub.dev. Here is what sets them apart:
package:a2ui_core (^0.3.0) library from the a2ui-project/a2ui specification (v0.9.1 and v1.0).WritableBinding form inputs, synchronous CheckRule validation, and NodeResolver list templates execute in 0ms in the exact same frame.bloc_signals_genui has zero Flutter dependencies. The exact same A2uiSurfaceBloc state machine drives both a 120 FPS Material 3 Flutter app and an interactive ANSI Terminal User Interface (TUI).bloc_signals_riverpod and bloc_signals_bloc, you can drop A2UI surfaces straight into an existing Riverpod or classic BLoC codebase without rewriting your architecture.We avoided inventing a custom JSON schema or maintaining a divergent protocol parser. Both packages build directly on Google's official package:a2ui_core Dart SDK for the A2UI (Agent-to-User Interface) protocol. (We even contributed upstream in a2ui-project/a2ui#2819 to lower a2ui_core's minimum SDK constraint to Dart ^3.5.0, opening the official A2UI engine to the entire Flutter 3.24+ ecosystem.)
When inspecting a2ui_core under the hood, one architectural detail stands out immediately: Google's a2ui_core already standardizes on preact_signals for its internal DataModel, GenericBinder, and reactive path bindings.
Wrapping a signal-powered protocol engine inside asynchronous StreamController pipes adds unnecessary translation layers. Because BlocSignal is built on the exact same reactive signal primitives, bloc_signals_genui connects to a2ui_core with zero impedance mismatch:
flowchart TD
Agent["AI Agent / LLM Backend\n(Streams A2UI v0.9 / v1.0 JSON via SSE)"]
Bloc["Edge Container: A2uiSurfaceBloc (Pure Dart)\n• Quarantines async stream with restartable() mutex\n• Sealed lifecycle: Initial → Streaming → Ready → Submitting\n• Bounded multi-surface LRU cache (maxSurfaces)"]
Core["Synchronous Reactive Core (a2ui_core + BlocSignal)\n• 0ms Two-Way WritableBinding DataModel sync\n• Synchronous CheckRule validation (isValid / errorText)\n• Reactive NodeResolver ChildList template expansion"]
Flutter["Flutter Renderer (bloc_signals_genui_flutter)\n• 16 Material 3 BasicCatalog widgets\n• Surgical leaf rebuilds via GenericBinder signals"]
TUI["ANSI Terminal Renderer (examples/genui_tui_agent)\n• UTF-8 box drawing & bracketed hotkeys\n• Interactive stdin prompts & CheckRule badges"]
Interop["Grand Central Interop\n• Riverpod 2/3 (.toProvider())\n• Classic BLoC & OpenTelemetry spans"]
Agent -->|"1. Async JSON Chunks (Network Perimeter)"| Bloc
Bloc -->|"2. 0ms Same-Frame Signal Graph"| Core
Core --> Flutter
Core --> TUI
Core --> Interop
In a real application, an AI-generated surface functions as a live interactive form, not a static poster. When a flight-booking agent streams a checkout card, the user still needs to type passenger names, toggle baggage checkboxes, pick seat classes, and see instant validation feedback before submitting their selection back to the agent.
bloc_signals_genui and bloc_signals_genui_flutter support the full 16-component A2UI BasicCatalog (Text, Row, Column, List, Tabs, Modal, Card, Divider, Icon, Image, Button, TextField, CheckBox, ChoicePicker, Slider, and DateTimeInput) with four synchronous capabilities:
WritableBinding Synchronization in 0msWhen an A2UI component binds a property to a JSON Pointer path in the surface's DataModel (for example, {"path": "/user/name"} in v0.9 or {"@path": "/user/name"} in v1.0), A2uiComponentContext exposes resolveBoundPath and updateBoundValue. Typing into an A2uiTextField, sliding an A2uiSlider, or picking a chip in A2uiChoicePicker mutates the underlying DataModel signal synchronously in 0ms. Only the leaf widget observing that path rebuilds—leaving the parent surface scaffold untouched.
CheckRule Validation UIA2UI allows components to declare client-side checks rules right in the wire JSON. As the user edits a bound field, GenericBinder evaluates those CheckRule conditions synchronously on every keystroke. A2uiComponentContext exposes isValid and validationErrors, displaying inline errorText on invalid inputs and automatically disabling A2uiButton submissions (onPressed: null) in the exact same frame when required checks fail—with zero LLM round-trips.
NodeResolver List TemplatesInstead of burning LLM output tokens streaming 20 repetitive component subtrees for a list of flights, the agent streams a single template component (flight_card) and a ChildList binding ({"componentId": "flight_card", "path": "/flights"}). A2uiSurfaceView mounts a2ui_core's NodeResolver to dynamically expand and scope each item's data context (/flights/0, /flights/1, ...) reactively as array elements stream in.
RpcHandler)Beyond standard action submissions (SubmitAction), A2uiSurfaceBloc wires MessageProcessor.rpc so the agent can invoke client-side renderer functions (CallRendererFunctionMessage) and your Dart code can invoke agent-side functions via bloc.callAgentFunction(...) with built-in timeout and lifecycle cancellation safety.
import 'package:bloc_signals_genui/bloc_signals_genui.dart';
// 1. Create the pure-Dart A2UI state machine
final surfaceBloc = A2uiSurfaceBloc(
maxSurfaces: 10,
maxResponseHistory: 100,
);
// 2. Ingest an incoming A2UI v0.9 stream (quarantined by restartable())
surfaceBloc.add(IngestStream(llmChunkStream));
// 3. Mutate bound form fields synchronously in 0ms
surfaceBloc.add(
const UpdateFormField(
path: '/user/name',
value: 'Randal L. Schwartz',
),
);
And because state transitions and form bindings are synchronous, your Flutter widget tests verify updates in a single tester.pump() without waiting on pumpAndSettle() microtask queues.
Streaming UI from an LLM introduces failure modes that traditional CRUD screens never encounter:
restartable()): What happens when an agent is halfway through streaming a complex itinerary surface and the user changes their mind and sends a new prompt? In naive stream setups, both streams interleave and corrupt the component tree. A2uiSurfaceBloc guards IngestStream with BlocSignal's streamless restartable() mutex transformer—subscribing to the replacement stream, preempting the stale stream immediately, and cleaning up resources without race conditions.user → model turn alternation. If your app optimistically appends a user prompt to chat history and the network drops before the first SSE chunk arrives (chunkCount == 0), leaving that unfulfilled user turn in history causes the user's next retry to crash with INVALID_ARGUMENT: Consecutive user turns are not allowed. Our architecture and reference apps bake in transactional zero-chunk turn pruning out of the box.A2uiSurfaceBloc separates navigation coordinates (activeSurfaceId, a 0ms ReadonlySignal<String?>) from per-surface content revisions (_surfaceVersions), so switching tabs via SelectSurface never triggers redundant binder rebuilds in split-pane views—while maxSurfaces automatically evicts the least-recently-used non-active surfaces so long sessions never leak memory.Because bloc_signals_genui has zero dependency on Flutter, your entire Generative UI state machine can run in CLI tools, backend Dart servers, automated test harnesses, or terminal applications.
To prove this architecture in the monorepo, we built two complete reference applications powered by the exact same A2uiSurfaceBloc and BasicCatalog payloads:
examples/genui_flight_booking: A Material 3 Flutter application rendering ChoicePicker seat chips, CheckBox preferences, DateTimeInput pickers, NodeResolver flight card templates (/flights), CheckRule passenger validation, and assignPreferredSeat RPC calls.examples/genui_tui_agent: An interactive ANSI Terminal User Interface (TUI) inspired by agy-cli that renders those exact same 16 BasicCatalog components, template paths, and CheckRule validation badges using UTF-8 box-drawing characters (┌─┐, │, └─┘), interactive stdin prompts, and bracketed action hotkeys ([1] Search Flights).# Run the interactive ANSI Terminal TUI agent in pure Dart:
dart run examples/genui_tui_agent/bin/main.dart --mock
Decoupling the A2UI state machine from the renderer means you can verify and fuzz-test complex multi-turn agent flows in pure dart test in milliseconds, without spinning up a simulator or headless browser.
Adopting a Generative UI library should not require rewriting your application's existing state management architecture.
Because A2uiSurfaceBloc extends BlocSignal, it plugs directly into the rest of the 13-package BlocSignal ecosystem:
bloc_signals_riverpod and call .toProvider(autoClose: true) on A2uiSurfaceBloc. Your Riverpod ConsumerWidgets and providers can watch the A2UI surface state or dispatch events through ref while A2uiSurfaceView handles the granular signal rendering:import 'package:bloc_signals_genui/bloc_signals_genui.dart';
import 'package:bloc_signals_riverpod/bloc_signals_riverpod.dart';
import 'package:riverpod/riverpod.dart';
// Expose A2uiSurfaceBloc as a native Riverpod NotifierProvider
final surfaceBloc = A2uiSurfaceBloc(maxSurfaces: 5);
final a2uiSurfaceProvider = surfaceBloc.toProvider(autoClose: true);
// Inside any Riverpod consumer:
// final surfaceState = ref.watch(a2uiSurfaceProvider);
// ref.read(a2uiSurfaceProvider.notifier).bloc.add(IngestStream(stream));
flutter_bloc? Bridge It cleanly: Use bloc_signals_bloc to bridge events and states bidirectionally with classic Bloc and Cubit instances.bloc_signals_otel and bloc_signals_devtools to trace every A2UI stream ingestion, chunk transition, and restartable() preemption span in OpenTelemetry and the Flutter DevTools extension with zero changes to your UI code.Both bloc_signals_genui (1.0.0) and bloc_signals_genui_flutter (1.0.0) are live on pub.dev with 100% test coverage and full support for Dart 3.5+ through Dart 3.13+:
# For pure-Dart, CLI, or TUI applications:
dart pub add bloc_signals_genui bloc_signals
# For Flutter applications:
flutter pub add bloc_signals_genui_flutter bloc_signals_genui bloc_signals_flutter
In follow-up articles in this series, we will dive deeper into each of these four pillars—including a hands-on walkthrough of building two-way validated A2UI forms with WritableBinding and CheckRule, wiring NodeResolver templates, and building headless/TUI agent runners in pure Dart.
bloc_signals_genui | bloc_signals_genui_flutter | a2ui_coreblocsignal.dev/docs/pkg-genui & blocsignal.dev/docs/pkg-genui-flutterRandalSchwartz/BlocSignal (genui_flight_booking & genui_tui_agent)
devchallengeWe're excited to announce the winners of the Hacktoberfest Open-Source AI Challenge Week 2: Side...
aiWhen we use generative AI to proofread our daily writing—whether technical reports, business...
aiA 2026 Harvard study of 718 companies found that AI agents write 30% more code and close zero additional tickets because review times jump 49%. Here is what happened when I put my 1986 Commitment Machine into live production across Gemini, Claude, and GPT-OSS.
claudeChoose Claude Models on Google Cloud (Google managed) for in-boundary security, regional data...
devchallengeWe are thrilled to announce the winners of the Sanity Challenge, our partnership with Sanity built...
geminiHow we built a 483 KB zero-WebKit macOS Universal Binary and a single-file Ubuntu HUD with...
Workflows from the Neura Market marketplace related to this Midjourney resource