Back to All Articles
flutterdartaiarchitectureui

Building Agent-Generated UIs in Flutter: Migrating to A2UI v0.9 and the New GenUI Architecture

Learn how to migrate your Flutter app to A2UI v0.9, decouple your LLM transport layer, and adopt prompt-first generative UI architectures.

2026-10-26
Building Agent-Generated UIs in Flutter: Migrating to A2UI v0.9 and the New GenUI Architecture

Let's look at how to structure a generative UI pipeline in Flutter by migrating an app to A2UI protocol v0.9 and decoupling your LLM transport layer.

What you'll learn:

  • How to replace rigid structured-output wrappers with a flexible prompt-first architecture.
  • Wiring up the new SurfaceController and A2uiTransportAdapter.
  • Updating your codebase to handle flat component schemas and clean property renames.
  • Managing agent communication via the genai_primitives package.

Ditching the ContentGenerator Wrapper

Older versions of the genui package hid network calls and prompt construction inside heavy wrapper classes like FirebaseAiContentGenerator. While convenient at first, this approach locked you into specific provider APIs and made debugging a nightmare when tokens failed to parse correctly.

Version 0.9 removes these wrappers entirely. Instead of letting a framework manage your LLM connection, you manage your agent client directly and pipe messages through a decoupled transport adapter.

DART
final catalog = BasicCatalogItems.asCatalog();

// Create a SurfaceController to manage generated UI state.
final surfaceController = SurfaceController(catalogs: [catalog]);

// Create a transport adapter to route chat messages to your agent.
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);
      }
    }

    // Send the raw token buffer to your custom LLM client.
    final response = await myAgentClient.sendRequest(buffer.toString());

    // Feed the response back into the adapter's input stream.
    adapter.addChunk(response);
  },
);

Speed Note: Decoupling the transport layer means you can swap out your LLM provider instantly without waiting for upstream package updates. Unit testing also becomes trivial since you can push mock chunks directly into adapter.addChunk().

Shifting to a Prompt-First Philosophy

Rigid API JSON modes often fight against an LLM's natural text-generation tendencies, especially when dealing with deeply nested component schemas. A2UI v0.9 moves the source of truth back into system instructions where models perform best.

Instead of relying on out-of-band schema parameters, you inject the layout rules directly into the LLM system prompt using the new PromptBuilder utility.

DART
final promptBuilder = PromptBuilder.chat(
  catalog: catalog,
  systemPromptFragments: [
    'You are a precise layout assistant. Return valid A2UI blocks.'
  ],
);

// Extract the plain text system prompt for your model's context window.
final systemPrompt = promptBuilder.systemPrompt;

Gotcha: You are now entirely responsible for making sure the generated system prompt lands inside your agent's initial context window. If your agent drops system instructions, your UI payload will fail to render.

Adopting Flat Component Schemas

Protocol v0.9 replaces nested wrapper keys with a flat discriminator structure. If your application manually constructs or parses A2UI payloads, you need to update your keys immediately.

Here is the quick breakdown of old vs. new component definitions:

DART
// The old nested style (v0.7)
final oldComponent = {
  "Text": {
    "text": "Hello World"
  }
};

// The new flat discriminator style (v0.9)
final newComponent = {
  "component": "Text",
  "text": "Hello World"
};

This flattening prevents deeply nested JSON trees from confusing smaller models and drastically simplifies custom client-side parsers.

Renaming Core Classes and Properties

The latest release cleans up the namespace by dropping the redundant GenUi prefix across nearly all core classes and standardizing property bindings.

Here is how your core controllers and widgets map after the upgrade:

DART
// Old class name -> New class name
// GenUiConversation -> Conversation
// GenUiController -> SurfaceController
// GenUiSurface -> Surface
// CoreCatalogItems -> BasicCatalogItems

final surfaceController = SurfaceController(
  catalogs: [BasicCatalogItems.asCatalog()],
);

final conversation = Conversation(
  surfaceController: surfaceController,
);

Property names inside components have also been aligned with standard Flutter naming conventions. For instance, distribution is now justify, alignment is align, and user triggers are handled via action.

Leveraging the genai_primitives Package

To keep things modular, the genui package no longer ships with its own isolated messaging types. Common primitives like ChatMessage, MessagePart, and ToolDefinition have been pulled out into the standalone genai_primitives package.

DART
import 'package:genai_primitives/genai_primitives.dart';

void handleIncomingMessage(ChatMessage message) {
  for (final part in message.parts) {
    print('Processing message part type: ${part.runtimeType}');
  }
}

This makes it easy to share chat structures across different AI features in your multi-platform app without dragging in entire rendering trees.

Summary

Migrating to A2UI v0.9 and the updated genui package gives you absolute control over your LLM lifecycle, prompt construction, and error boundaries. By shifting to flat component definitions and prompt-first generation, your agentic UIs will be significantly more reliable and easier to debug.

Next Steps:

  • Run through the official Flutter GenUI codelab to build a working prototype in under 90 minutes.
  • Refactor your custom transport adapters to handle streamed chunks via adapter.addChunk.
  • Audit your custom component catalogs for the new flat discriminator format.