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
SurfaceControllerandA2uiTransportAdapter. - Updating your codebase to handle flat component schemas and clean property renames.
- Managing agent communication via the
genai_primitivespackage.
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.
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.
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:
// 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:
// 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.
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.