Skip to content
Blog

GenUI Architecture Decoupling: What Changed in Flutter's GenUI Package

The latest genui release removes ContentGenerator and splits the framework into Engine, Transport, and Facade layers. Learn how the architecture decoupling and Prompt First approach change how Flutter apps talk to LLMs.

Published on • October 2, 2026

AI Assistant

Generative UI (GenUI) lets an AI agent decide not just what content to show, but how it should be laid out and made interactive. In Flutter, that’s powered by the A2UI protocol and the genui package. The latest release reworks both — driven by A2UI v0.9 — and the biggest change is architectural: genui is now decoupled.

Source: New updates to A2UI and Flutter’s GenUI package, The Flutter Blog, May 14, 2026.

From ContentGenerator to three layers

In previous versions, genui relied on classes built around ContentGenerator, which hid prompt construction, LLM network calls, and response parsing behind a single abstraction. The new release removes ContentGenerator entirely and splits the framework into distinct layers:

  • Engine (SurfaceController) — manages the state and rendering of your UI.
  • Transport (A2uiTransportAdapter) — streams messages between the agent and renderer.
  • Facade (Conversation) — provides a high-level API for managing chat states.

Because ContentGenerator is gone, the provider-specific wrapper packages (genui_dartantic, genui_google_generative_ai, genui_firebase_ai) are no longer needed and disappear from your dependency tree.

The old way:

final generator = FirebaseAiContentGenerator(
  catalog: CoreCatalogItems.asCatalog(),
  systemInstruction: 'You are a helpful assistant.',
);

final conversation = GenUiConversation(
  genUiManager: GenUiManager(catalog: catalog),
  contentGenerator: generator,
);

The new way:

final catalog = BasicCatalogItems.asCatalog();

final surfaceController = SurfaceController(catalogs: [catalog]);

late final adapter = A2uiTransportAdapter(
  onSend: (ChatMessage msg) async {
    final buffer = StringBuffer();
    for (final part in msg.parts) {
      if (part.isUiInteractionPart) {
        buffer.write(part.asUiInteractionPart!.interaction);
      } else if (part is genui.TextPart) {
        buffer.write(part.text);
      }
    }
    final response = await myAgentClient.sendRequest(buffer.toString());
    adapter.addChunk(response);
  },
);

Yes, there’s a bit more wiring code. But the trade-offs are concrete:

  • You can set up your agent however you like, hold it where you prefer, and manage its lifecycle yourself.
  • You can use nearly any AI source without waiting for a package update shipping a new ContentGenerator.
  • The agent connection and genui are loosely coupled — only tokens move back and forth.
  • Testing gets simpler: genui accepts tokens directly, so a mock agent or hard-coded test works fine.

Going Prompt First

The second big shift is from “Structured Output First” to “Prompt First.”

Previously, genui leaned on provider-level API constraints like JSON Mode and function-calling schemas to force the model into valid UI structures. That produced predictable JSON, but deeply nested schemas could confuse models, it capped the size and complexity of your catalog, and the constraints lived in the network layer where they were hard to read and tweak.

Prompt First moves the source of truth back to system instructions. The UI schema and A2UI rules are injected as plain text into the LLM’s prompt, which is where modern models are optimized to follow detailed instructions. Because the schema is now plain text, you can edit it to suit your app.

The genui package ships a PromptBuilder to help:

final promptBuilder = PromptBuilder.chat(
  catalog: catalog,
  systemPromptFragments: ['You are a helpful assistant.'],
);

// Then pass promptBuilder.systemPrompt to your LLM.

Your app is now responsible for getting the right prompt into the context window — PromptBuilder does the heavy lifting of generating schema definitions and A2UI formatting rules.

Breaking changes in A2UI v0.9

If your code hand-builds A2UI JSON, note these adjustments:

  • beginRendering is now createSurface.
  • Components use a flat discriminator: {"component": "Text", "text": "Hello"} instead of nested keys.
  • Data bindings simplify to { "path": "/path/to/var" }.
  • Property renames: distribution → justify, alignment → align, usageHint → variant, text → value (in TextField), userAction → action.

Class names lost their GenUi prefix too: GenUiConversation → Conversation, GenUiController → SurfaceController, GenUiSurface → Surface, GenUiTransport → Transport. CoreCatalogItems became BasicCatalogItems, and GenUiFunctionDeclaration became ClientFunction to align with standard LLM function-calling terms.

genai_primitives moves out

genui no longer ships its own messaging types. The new genai_primitives package provides primitive types like ChatMessage, MessagePart, and ToolDefinition — flexible enough to reuse across other GenAI packages.

Should you migrate?

If you’re on genui v0.7.0, budget for a real migration: dependency cleanup, swapping ContentGenerator for a TransportAdapter, and rewriting any code that constructs A2UI payloads by hand. In return you get a framework that no longer boxes you into one LLM provider, tests without a live model, and a prompt you can actually read and tune.

The Flutter team maintains a migration guide from v0.7.0 to v0.9.0 covering dependency cleanup and rewiring chat loops, plus an introductory codelab if you’re starting fresh.