1. Einführung
In diesem Codelab erstellen Sie eine Aufgabenlisten-App mit Flutter, Firebase AI Logic und dem neuen genui-Paket. Sie beginnen mit einer textbasierten Chat-App, erweitern sie mit GenUI, damit der Agent seine eigene Benutzeroberfläche erstellen kann, und entwickeln schließlich eine eigene interaktive UI-Komponente, die Sie und der Agent direkt bearbeiten können.

Aufgaben
- Einfache Chatoberfläche mit Flutter und Firebase AI Logic erstellen
genui-Paket einbinden, um KI-basierte Oberflächen zu generieren- Fortschrittsanzeige hinzufügen, um anzuzeigen, wann die App auf eine Antwort des Agents wartet
- Erstellen Sie eine benannte Oberfläche und zeigen Sie sie an einer bestimmten Stelle in der Benutzeroberfläche an.
- Benutzerdefinierte GenUI-Katalogkomponente erstellen, mit der Sie die Darstellung von Aufgaben steuern können
Voraussetzungen
- Ein Webbrowser wie Chrome
- Das Flutter SDK ist lokal installiert.
- Die Firebase CLI ist installiert und konfiguriert.
Dieses Codelab richtet sich an fortgeschrittene Flutter-Entwickler.
2. Hinweis
Flutter-Projekt einrichten
Öffnen Sie das Terminal und führen Sie flutter create aus, um ein neues Projekt zu erstellen:
flutter create intro_to_genui
cd intro_to_genui
Fügen Sie Ihrem Flutter-Projekt die erforderlichen Abhängigkeiten hinzu:
flutter pub add firebase_core firebase_ai genui json_schema_builder
Der endgültige dependencies-Abschnitt sollte so aussehen (Versionsnummern können leicht abweichen):
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
firebase_core: ^4.5.0
firebase_ai: ^3.9.0
genui: ^0.8.0
json_schema_builder: ^0.1.3
Führen Sie flutter pub get aus, um alle Pakete herunterzuladen.
APIs und Firebase aktivieren
Wenn Sie das firebase_ai-Paket verwenden möchten, müssen Sie zuerst Firebase AI Logic in Ihrem Projekt aktivieren.
- Rufen Sie in der Firebase Console Firebase AI Logic auf.
- Klicken Sie auf Jetzt starten, um den geführten Workflow zu starten.
- Folgen Sie der Anleitung auf dem Bildschirm, um Ihr Projekt einzurichten.
Weitere Informationen finden Sie in der Anleitung zum Hinzufügen von Firebase zu einer Flutter-App.
Sobald die APIs aktiv sind, initialisieren Sie Firebase in Ihrer Flutter-App mit der FlutterFire CLI:
flutterfire configure
Wählen Sie Ihr Firebase-Projekt aus und folgen Sie der Anleitung, um es für Ihre Zielplattformen (z. B. Android, iOS, Web) zu konfigurieren. Für dieses Codelab benötigen Sie nur das Flutter SDK und Chrome auf Ihrem Computer. Die App funktioniert aber auch auf anderen Plattformen.
3. Einfache Chatoberfläche erstellen
Bevor Sie Generative UI einführen können, muss Ihre App eine Grundlage haben: eine einfache textbasierte Chatanwendung, die auf Firebase AI Logic basiert. Um schnell loszulegen, kopieren Sie die gesamte Einrichtung für die Chatoberfläche und fügen Sie sie ein.

Widget für Sprechblasen erstellen
Damit SMS vom Nutzer und vom Agent angezeigt werden können, benötigt Ihre App ein Widget. Erstellen Sie eine neue Datei mit dem Namen lib/message_bubble.dart und fügen Sie die folgende Klasse hinzu:
import 'package:flutter/material.dart';
class MessageBubble extends StatelessWidget {
final String text;
final bool isUser;
const MessageBubble({super.key, required this.text, required this.isUser});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final bubbleColor = isUser
? colorScheme.primary
: colorScheme.surfaceContainerHighest;
final textColor = isUser
? colorScheme.onPrimary
: colorScheme.onSurfaceVariant;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 8.0),
child: Column(
crossAxisAlignment: isUser
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: isUser
? MainAxisAlignment.end
: MainAxisAlignment.start,
children: [
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 12.0,
),
decoration: BoxDecoration(
color: bubbleColor,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(20),
topRight: const Radius.circular(20),
bottomLeft: Radius.circular(isUser ? 20 : 0),
bottomRight: Radius.circular(isUser ? 0 : 20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(20),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
gradient: isUser
? LinearGradient(
colors: [
colorScheme.primary,
colorScheme.primary.withAlpha(200),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
),
child: Text(
text,
style: theme.textTheme.bodyLarge?.copyWith(
color: textColor,
height: 1.3,
),
),
),
),
],
),
const SizedBox(height: 2),
],
),
);
}
}
MessageBubble ist ein StatelessWidget, in dem eine einzelne Chatnachricht angezeigt wird. Es wird später in diesem Codelab verwendet, um Nachrichten von Ihnen und dem Agenten anzuzeigen. Im Grunde ist es aber nur ein Text-Widget.
Chat-Benutzeroberfläche in main.dart implementieren
Ersetzen Sie den gesamten Inhalt von lib/main.dart durch diese vollständige Text-Chatbot-Implementierung:
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:intro_to_genui/message_bubble.dart';
import 'firebase_options.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Just Today',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
sealed class ConversationItem {}
class TextItem extends ConversationItem {
final String text;
final bool isUser;
TextItem({required this.text, this.isUser = false});
}
class _MyHomePageState extends State<MyHomePage> {
final List<ConversationItem> _items = [];
final _textController = TextEditingController();
final _scrollController = ScrollController();
late final ChatSession _chatSession;
@override
void initState() {
super.initState();
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-3-flash-preview',
);
_chatSession = model.startChat();
_chatSession.sendMessage(Content.text(systemInstruction));
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
@override
void dispose() {
_textController.dispose();
_scrollController.dispose();
super.dispose();
}
Future<void> _addMessage() async {
final text = _textController.text;
if (text.trim().isEmpty) {
return;
}
_textController.clear();
setState(() {
_items.add(TextItem(text: text, isUser: true));
});
_scrollToBottom();
final response = await _chatSession.sendMessage(Content.text(text));
if (response.text?.isNotEmpty ?? false) {
setState(() {
_items.add(TextItem(text: response.text!, isUser: false));
});
_scrollToBottom();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Just Today'),
),
body: Column(
children: [
Expanded(
child: ListView(
controller: _scrollController,
padding: const EdgeInsets.all(16),
children: [
for (final item in _items)
switch (item) {
TextItem() => MessageBubble(
text: item.text,
isUser: item.isUser,
),
},
],
),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _textController,
onSubmitted: (_) => _addMessage(),
decoration: const InputDecoration(
hintText: 'Enter a message',
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _addMessage,
child: const Text('Send'),
),
],
),
),
),
],
),
);
}
}
const systemInstruction = '''
## PERSONA
You are an expert task planner.
## GOAL
Work with me to produce a list of tasks that I should do today, and then track
the completion status of each one.
## RULES
Talk with me only about tasks that I should do today.
Do not engage in conversation about any other topic.
Do not offer suggestions unless I ask for them.
Do not offer encouragement unless I ask for it.
Do not offer advice unless I ask for it.
Do not offer opinions unless I ask for them.
## PROCESS
### Planning
* Ask me for information about tasks that I should do today.
* Synthesize a list of tasks from that information.
* Ask clarifying questions if you need to.
* When you have a list of tasks that you think I should do today, present it
to me for review.
* Respond to my suggestions for changes, if I have any, until I accept the
list.
### Tracking
* Once the list is accepted, ask me to let you know when individual tasks are
complete.
* If I tell you a task is complete, mark it as complete.
* Once all tasks are complete, send a message acknowledging that, and then
end the conversation.
''';
Mit der Datei main.dart, die Sie gerade kopiert und eingefügt haben, wird ein einfacher ChatSession mit Firebase AI Logic und dem Prompt in systemInstruction eingerichtet. Es verwaltet die Gesprächsrunden, indem es eine Liste mit TextItem-Elementen führt und sie zusammen mit Nutzeranfragen über das MessageBubble-Widget anzeigt, das Sie zuvor erstellt haben.
Bevor Sie fortfahren, sollten Sie Folgendes prüfen:
- Mit der Methode
initStatewird die Verbindung zu Firebase AI Logic eingerichtet. - Die App bietet ein
TextFieldund eine Schaltfläche zum Senden von Nachrichten an den Agenten. - Mit der Methode
_addMessagewird die Nachricht des Nutzers an den Agent gesendet. - In der Liste
_itemswird der Unterhaltungsverlauf gespeichert. - Nachrichten werden in einem
ListViewmit demMessageBubble-Widget angezeigt.
Anwendung testen
Jetzt können Sie die App ausführen und testen.
flutter run -d chrome
Chatten Sie mit dem Agenten über einige Aufgaben, die Sie heute erledigen möchten. Eine rein textbasierte Benutzeroberfläche kann zwar die Aufgabe erledigen, aber GenUI kann die Nutzung einfacher und schneller machen.
4. GenUI-Paket einbinden
Jetzt ist es an der Zeit, von Nur-Text auf die generative Benutzeroberfläche umzustellen. Sie ersetzen die grundlegende Firebase-Messaging-Schleife durch GenUI-Objekte Conversation, Catalog und SurfaceController. So kann das KI-Modell tatsächliche Flutter-Widgets im Chatstream instanziieren.

Das genui-Paket enthält fünf Klassen, die Sie in diesem Codelab verwenden:
- Die vom Modell generierte
SurfaceController-Benutzeroberfläche wird auf dem Bildschirm angezeigt. A2uiTransportAdapterverbindet interne GenUI-Anfragen mit einem beliebigen externen Sprachmodell.Conversationkapselt den Controller und den Transportadapter mit einer einzigen, einheitlichen API für Ihre Flutter-App.Catalogbeschreibt die Widgets und Attribute, die dem Sprachmodell zur Verfügung stehen.Surfaceist ein Widget, in dem die vom Modell generierte Benutzeroberfläche angezeigt wird.
Generierte Surface anzeigen
Der vorhandene Code enthält eine TextItem-Klasse, die eine einzelne SMS in der Unterhaltung darstellt. Fügen Sie eine weitere Klasse hinzu, um ein vom Agent erstelltes Surface darzustellen:
class SurfaceItem extends ConversationItem {
final String surfaceId;
SurfaceItem({required this.surfaceId});
}
GenUI-Bausteine initialisieren
Importieren Sie oben in lib/main.dart die genui-Bibliothek:
import 'package:genui/genui.dart' hide TextPart;
import 'package:genui/genui.dart' as genui;
Sowohl das Paket genui als auch das Paket firebase_ai enthalten eine TextPart-Klasse. Wenn Sie genui auf diese Weise importieren, wird die Version von TextPart als genui.TextPart in einem Namespace platziert, um Namenskonflikte zu vermeiden.
Deklarieren Sie die wichtigsten funktionalen Controller in _MyHomePageState nach _chatSession:
class _MyHomePageState extends State<MyHomePage> {
// ... existing members
late final ChatSession _chatSession;
// Add GenUI controllers
late final SurfaceController _controller;
late final A2uiTransportAdapter _transport;
late final Conversation _conversation;
late final Catalog catalog;
Aktualisieren Sie als Nächstes initState, um die Controller der GenUI-Bibliothek vorzubereiten.
Entfernen Sie diese Zeile aus initState:
_chatSession.sendMessage(Content.text(systemInstruction));
Fügen Sie dann den folgenden Code hinzu:
@override
void initState() {
// ... existing code ...
// Initialize the GenUI Catalog.
// The genui package provides a default set of primitive widgets (like text
// and basic buttons) out of the box using this class.
catalog = BasicCatalogItems.asCatalog();
// Create a SurfaceController to manage the state of generated surfaces.
_controller = SurfaceController(catalogs: [catalog]);
// Create a transport adapter that will process messages to and from the
// agent, looking for A2UI messages.
_transport = A2uiTransportAdapter(onSend: _sendAndReceive);
// Link the transport and SurfaceController together in a Conversation,
// which provides your app a unified API for interacting with the agent.
_conversation = Conversation(
controller: _controller,
transport: _transport,
);
}
Mit diesem Code wird eine Conversation-Fassade erstellt, die den Controller und den Adapter verwaltet. Über diese Unterhaltung erhält Ihre App einen Stream von Ereignissen, mit denen sie nachvollziehen kann, was der Agent erstellt, sowie eine Methode zum Senden von Nachrichten an den Agenten.
Erstellen Sie als Nächstes einen Listener für Unterhaltungsereignisse. Dazu gehören oberflächenbezogene Ereignisse sowie Ereignisse für SMS und Fehler:
@override
void initState() {
// ... existing code ...
// Listen to GenUI stream events to update the UI
_conversation.events.listen((event) {
setState(() {
switch (event) {
case ConversationSurfaceAdded added:
_items.add(SurfaceItem(surfaceId: added.surfaceId));
_scrollToBottom();
case ConversationSurfaceRemoved removed:
_items.removeWhere(
(item) =>
item is SurfaceItem && item.surfaceId == removed.surfaceId,
);
case ConversationContentReceived content:
_items.add(TextItem(text: content.text, isUser: false));
_scrollToBottom();
case ConversationError error:
debugPrint('GenUI Error: ${error.error}');
default:
}
});
});
}
Erstellen Sie schließlich den Systemprompt und senden Sie ihn an den Agent:
@override
void initState() {
// ... existing code ...
// Create the system prompt for the agent, which will include this app's
// system instruction as well as the schema for the catalog.
final promptBuilder = PromptBuilder.chat(
catalog: catalog,
systemPromptFragments: [systemInstruction],
);
// Send the prompt into the Conversation, which will subsequently route it
// to Firebase using the transport mechanism.
_conversation.sendRequest(
ChatMessage.system(promptBuilder.systemPromptJoined()),
);
}
Displayoberflächen
Aktualisieren Sie als Nächstes die Methode build von ListView, um die SurfaceItem in der Liste _items anzuzeigen:
Expanded(
child: ListView(
controller: _scrollController,
padding: const EdgeInsets.all(16),
children: [
for (final item in _items)
switch (item) {
TextItem() => MessageBubble(
text: item.text,
isUser: item.isUser,
),
// New!
SurfaceItem() => Surface(
surfaceContext: _controller.contextFor(
item.surfaceId,
),
),
},
],
),
),
Der Konstruktor für das Surface-Widget verwendet ein surfaceContext, das angibt, welche Oberfläche angezeigt werden soll. Das zuvor erstellte SurfaceController, _controller, enthält die Definition und den Status für jede Oberfläche und sorgt dafür, dass es bei einer Aktualisierung neu erstellt wird.
GenUI mit Firebase AI Logic verbinden
Das genui-Paket verwendet das „Bring Your Own Model“-Konzept. Das bedeutet, dass Sie selbst bestimmen, welches LLM für Ihre Anwendung verwendet wird. In diesem Fall verwenden Sie Firebase AI Logic, das Paket ist jedoch für die Verwendung mit einer Vielzahl von Agents und Anbietern konzipiert.
Diese Freiheit bringt jedoch auch zusätzliche Verantwortung mit sich: Sie müssen die vom genui-Paket generierten Nachrichten an den von Ihnen ausgewählten Agent senden und die Antworten des Agents zurück an genui senden.
Dazu definieren Sie die Methode _sendAndReceive, auf die im Code für den vorherigen Schritt verwiesen wird. Fügen Sie diesen Code zu MyHomePageState hinzu:
Future<void> _sendAndReceive(ChatMessage msg) async {
final buffer = StringBuffer();
// Reconstruct the message part fragments
for (final part in msg.parts) {
if (part.isUiInteractionPart) {
buffer.write(part.asUiInteractionPart!.interaction);
} else if (part is genui.TextPart) {
buffer.write(part.text);
}
}
if (buffer.isEmpty) {
return;
}
final text = buffer.toString();
// Send the string to Firebase AI Logic.
final response = await _chatSession.sendMessage(Content.text(text));
if (response.text?.isNotEmpty ?? false) {
// Feed the response back into GenUI's transportation layer
_transport.addChunk(response.text!);
}
}
Diese Methode wird vom genui-Paket aufgerufen, wenn eine Nachricht an den Agent gesendet werden muss. Durch den Aufruf von addChunk am Ende der Methode wird die Antwort des Agenten wieder in das genui-Paket eingespeist, sodass sie verarbeitet und eine Benutzeroberfläche generiert werden kann.
Ersetzen Sie schließlich Ihre vorhandene _addMessage-Methode vollständig durch diese neue Version, damit Nachrichten an Conversation statt direkt an Firebase weitergeleitet werden:
Future<void> _addMessage() async {
final text = _textController.text;
if (text.trim().isEmpty) {
return;
}
_textController.clear();
setState(() {
_items.add(TextItem(text: text, isUser: true));
});
_scrollToBottom();
// Send the user's input through GenUI instead of directly to Firebase.
await _conversation.sendRequest(ChatMessage.user(text));
}
Geschafft! Versuchen Sie noch einmal, die App auszuführen. Neben Textnachrichten sehen Sie, wie der Agent Benutzeroberflächen wie Schaltflächen und Text-Widgets generiert.
Sie können den Agent auch bitten, die Benutzeroberfläche auf eine bestimmte Weise darzustellen. Sie können zum Beispiel Folgendes eingeben: „Zeige mir meine Aufgaben in einer Spalte an, mit einer Schaltfläche, um jede Aufgabe als erledigt zu markieren.“
5. Wartestatus hinzufügen
Die LLM-Generierung ist asynchron. Während auf eine Antwort gewartet wird, müssen die Eingabeschaltflächen in der Chatoberfläche deaktiviert und eine Fortschrittsanzeige eingeblendet werden, damit der Nutzer weiß, dass GenUI Inhalte erstellt. Glücklicherweise bietet das Paket genui eine Listenable, mit der Sie den Status der Unterhaltung verfolgen können. Dieser ConversationState-Wert enthält die Eigenschaft isWaiting, mit der bestimmt wird, ob das Modell Inhalte generiert.
Schließen Sie die Eingabesteuerelemente in ein ValueListenableBuilder-Tag ein.
Erstellen Sie ein ValueListenableBuilder, das das Row (mit Ihrem TextField und ElevatedButton) am Ende von lib/main.dart umschließt, um sich das _conversation.state anzuhören. Wenn Sie state.isWaiting prüfen, können Sie die Eingabe deaktivieren, während das Modell Inhalte generiert.
ValueListenableBuilder<ConversationState>(
valueListenable: _conversation.state,
builder: (context, state, child) {
return Row(
children: [
Expanded(
child: TextField(
controller: _textController,
// Also disable the Enter key submission when waiting!
onSubmitted: state.isWaiting ? null : (_) => _addMessage(),
decoration: const InputDecoration(
hintText: 'Enter a message',
),
),
),
const SizedBox(width: 8),
ElevatedButton(
// Disable the send button when the model is generating
onPressed: state.isWaiting ? null : _addMessage,
child: const Text('Send'),
),
],
);
},
),
Fortschrittsanzeige hinzufügen
Schließen Sie das Column-Hauptwidget in ein Stack ein und fügen Sie das LinearProgressIndicator als zweites untergeordnetes Element dieses Stacks hinzu, das unten verankert ist. Wenn Sie fertig sind, sollte das body Ihres Scaffold so aussehen:
body: Stack( // New!
children: [
Column(
children: [
Expanded(
child: ListView(
controller: _scrollController,
padding: const EdgeInsets.all(16),
children: [
for (final item in _items)
switch (item) {
TextItem() => MessageBubble(
text: item.text,
isUser: item.isUser,
),
SurfaceItem() => Surface(
surfaceContext: _controller.contextFor(
item.surfaceId,
),
),
},
],
),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ValueListenableBuilder<ConversationState>(
valueListenable: _conversation.state,
builder: (context, state, child) {
return Row(
children: [
Expanded(
child: TextField(
controller: _textController,
onSubmitted:
state.isWaiting ? null : (_) => _addMessage(),
decoration: const InputDecoration(
hintText: 'Enter a message',
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: state.isWaiting ? null : _addMessage,
child: const Text('Send'),
),
],
);
},
),
),
),
],
),
// Listen to the state again, this time to render a progress indicator
ValueListenableBuilder<ConversationState>(
valueListenable: _conversation.state,
builder: (context, state, child) {
if (state.isWaiting) {
return const LinearProgressIndicator();
}
return const SizedBox.shrink();
},
),
],
),
6. GenUI-Oberfläche beibehalten
Bisher wurde die Aufgabenliste im scrollenden Chatstream gerendert. Jede neue Nachricht oder Oberfläche wurde der Liste hinzugefügt, sobald sie einging. Im nächsten Schritt erfahren Sie, wie Sie eine Oberfläche benennen und an einer bestimmten Stelle in der Benutzeroberfläche anzeigen.
Deklarieren Sie zuerst oben in main.dart vor void main() eine Konstante, die als Oberflächen-ID verwendet werden soll:
const taskDisplaySurfaceId = 'task_display';
Zweitens müssen Sie die switch-Anweisung im Conversation-Listener aktualisieren, damit keine Oberfläche mit dieser ID zu _items hinzugefügt wird:
case ConversationSurfaceAdded added:
if (added.surfaceId != taskDisplaySurfaceId) {
_items.add(SurfaceItem(surfaceId: added.surfaceId));
_scrollToBottom();
}
Öffnen Sie als Nächstes die Layoutstruktur Ihres Widget-Baums, um einen Bereich für die angepinnte Oberfläche direkt über Ihrem Chatverlauf zu erstellen. Fügen Sie diese beiden Widgets als erste untergeordnete Elemente des Haupt-Column hinzu:
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.topLeft,
child: Surface(
surfaceContext: _controller.contextFor(
taskDisplaySurfaceId,
),
),
),
),
const Divider(),
Bisher hatte Ihr Agent kostenlose Hand, Oberflächen nach Bedarf zu erstellen und zu verwenden. Um dem Modell genauere Anweisungen zu geben, müssen Sie den Systemprompt noch einmal aufrufen. Fügen Sie den folgenden ## USER INTERFACE-Abschnitt am Ende des Prompts ein, der in der Konstanten systemInstruction gespeichert ist:
const systemInstruction = '''
// ... existing prompt content ...
## USER INTERFACE
* To display the list of tasks create one and only one instance of the
TaskDisplay catalog item. Use "$taskDisplaySurfaceId" as its surface ID.
* Update $taskDisplaySurfaceId as necessary when the list changes.
* $taskDisplaySurfaceId must include a button for each task that I can use
to mark it complete. When I use that button to mark a task complete, it
should send you a message indicating what I've done.
* Avoid repeating the same information in a single message.
* When responding with text, rather than A2UI messages, be brief.
''';
Es ist wichtig, Ihrem Agenten klare Anweisungen dazu zu geben, wann und wie er UI-Oberflächen verwenden soll. Wenn Sie dem Agent mitteilen, dass er ein bestimmtes Katalogelement und eine bestimmte Oberflächen-ID verwenden soll (und eine einzelne Instanz wiederverwenden soll), können Sie dafür sorgen, dass die gewünschte Benutzeroberfläche erstellt wird.
Es ist noch einiges zu tun, aber Sie können Ihre App noch einmal ausführen, um zu sehen, wie der KI-Agent die Aufgabenanzeige oben in der Benutzeroberfläche erstellt.
7. Benutzerdefiniertes Katalog-Widget erstellen
Zu diesem Zeitpunkt ist das Katalogelement TaskDisplay noch nicht vorhanden. In den nächsten Schritten beheben Sie das Problem, indem Sie ein Datenschema, eine Klasse zum Parsen dieses Schemas, ein Widget und das Katalogelement erstellen, das alles zusammenfügt.
Erstellen Sie zuerst eine Datei mit dem Namen task_display.dart und fügen Sie die folgenden Importe hinzu:
import 'package:flutter/material.dart';
import 'package:genui/genui.dart';
import 'package:json_schema_builder/json_schema_builder.dart';
Datenschema erstellen
Definieren Sie als Nächstes das Datenschema, das der Agent bereitstellt, wenn er eine Aufgabenanzeige erstellen möchte. Dabei werden einige spezielle Konstruktoren aus dem json_schema_builder-Paket verwendet. Im Grunde definieren Sie aber nur ein JSON-Schema, das in Nachrichten an und von dem Agent verwendet wird.
Beginnen Sie mit einem einfachen S.object, das auf den Komponentennamen verweist:
final taskDisplaySchema = S.object(
properties: {
'component': S.string(enumValues: ['TaskDisplay']),
},
);
Fügen Sie als Nächstes title, tasks, name, isCompleted und completeAction zu den Schemaattributen hinzu.
final taskDisplaySchema = S.object(
properties: {
'component': S.string(enumValues: ['TaskDisplay']),
'title': S.string(description: 'The title of the task list'),
'tasks': S.list(
description: 'A list of tasks to be completed today',
items: S.object(
properties: {
'name': S.string(description: 'The name of the task to be completed'),
'isCompleted': S.boolean(
description: 'Whether the task is completed',
),
'completeAction': A2uiSchemas.action(
description:
'The action performed when the user has completed the task.',
),
},
),
),
},
);
Sehen Sie sich das Attribut completeAction an. Es wird mit A2uiSchemas.action erstellt, dem Konstruktor für eine Schema-Property, die eine A2UI-Aktion darstellt. Wenn Sie dem Schema eine Aktion hinzufügen, teilt die App dem Agenten im Grunde mit: „Hey, wenn du mir eine Aufgabe gibst, gib mir auch den Namen und die Metadaten für eine Aktion, mit der ich dir mitteilen kann, dass die Aufgabe erledigt ist.“ Später ruft die App diese Aktion auf, wenn der Nutzer auf ein Kästchen tippt.
Fügen Sie dem Schema als Nächstes required-Felder hinzu. Damit wird der Agent angewiesen, bestimmte Eigenschaften immer auszufüllen. In diesem Fall ist jede Eigenschaft erforderlich.
final taskDisplaySchema = S.object(
properties: {
'component': S.string(enumValues: ['TaskDisplay']),
'title': S.string(description: 'The title of the task list'),
'tasks': S.list(
description: 'A list of tasks to be completed today',
items: S.object(
properties: {
'name': S.string(description: 'The name of the task to be completed'),
'isCompleted': S.boolean(
description: 'Whether the task is completed',
),
'completeAction': A2uiSchemas.action(
description:
'The action performed when the user has completed the task.',
),
},
// New!
required: ['name', 'isCompleted', 'completeAction'],
),
),
},
// New!
required: ['title', 'tasks'],
);
Klassen zum Parsen von Daten erstellen
Wenn Instanzen dieser Komponente erstellt werden, sendet der Agent Daten, die dem Schema entsprechen. Fügen Sie zwei Klassen hinzu, um das eingehende JSON in stark typisierte Dart-Objekte zu parsen. Beachten Sie, wie _TaskDisplayData die Stammstruktur verarbeitet, während die Analyse des inneren Arrays an _TaskData delegiert wird.
class _TaskData {
final String name;
final bool isCompleted;
final String actionName;
final JsonMap actionContext;
_TaskData({
required this.name,
required this.isCompleted,
required this.actionName,
required this.actionContext,
});
factory _TaskData.fromJson(Map<String, Object?> json) {
try {
final action = json['completeAction']! as JsonMap;
final event = action['event']! as JsonMap;
return _TaskData(
name: json['name'] as String,
isCompleted: json['isCompleted'] as bool,
actionName: event['name'] as String,
actionContext: event['context'] as JsonMap,
);
} catch (e) {
throw Exception('Invalid JSON for _TaskData: $e');
}
}
}
class _TaskDisplayData {
final String title;
final List<_TaskData> tasks;
_TaskDisplayData({required this.title, required this.tasks});
factory _TaskDisplayData.fromJson(Map<String, Object?> json) {
try {
return _TaskDisplayData(
title: (json['title'] as String?) ?? 'Tasks',
tasks: (json['tasks'] as List<Object?>)
.map((e) => _TaskData.fromJson(e as Map<String, Object?>))
.toList(),
);
} catch (e) {
throw Exception('Invalid JSON for _TaskDisplayData: $e');
}
}
}
Wenn Sie schon einmal mit Flutter gearbeitet haben, sind diese Klassen wahrscheinlich ähnlich wie die, die Sie erstellt haben. Sie akzeptieren ein JsonMap und geben ein stark typisiertes Objekt mit aus JSON geparsten Daten zurück.
Sehen Sie sich die Felder actionName und actionContext in _TaskData an. Sie werden aus der completeAction-Property des JSON extrahiert und enthalten den Namen der Aktion und den zugehörigen Datenkontext (einen Verweis auf den Speicherort der Aktion im Datenmodell von GenUI). Sie werden später zum Erstellen eines UserActionEvent verwendet.
Das Datenmodell ist ein zentraler, beobachtbarer Speicher für den gesamten dynamischen UI-Status, der von der genui-Bibliothek verwaltet wird. Wenn der Agent eine UI-Komponente aus dem Katalog erstellt, wird auch ein Datenobjekt erstellt, das dem Schema der Komponente entspricht. Dieses Datenobjekt wird im Datenmodell im Client gespeichert, damit es zum Erstellen von Widgets verwendet und in späteren Nachrichten an den Agenten referenziert werden kann (z. B. das completeAction, das Sie gleich mit einem Widget verbinden).
Füge das Widget hinzu.
Erstellen Sie nun ein Widget, um die Liste anzuzeigen. Sie sollte eine Instanz der _TaskDisplayData-Klasse und einen Callback akzeptieren, der aufgerufen wird, wenn eine Aufgabe abgeschlossen ist.
class _TaskDisplay extends StatelessWidget {
final _TaskDisplayData data;
final void Function(_TaskData) onCompleteTask;
const _TaskDisplay({required this.data, required this.onCompleteTask});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
data.title,
style: Theme.of(context).textTheme.titleLarge,
),
),
...data.tasks.map(
(task) => CheckboxListTile(
title: Text(
task.name,
style: TextStyle(
decoration: task.isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
value: task.isCompleted,
onChanged: task.isCompleted
? null
: (val) {
if (val == true) {
onCompleteTask(task);
}
},
),
),
],
);
}
}
CatalogItem erstellen
Nachdem Sie das Schema, den Parser und das Widget erstellt haben, können Sie jetzt ein CatalogItem erstellen, um alles zu verknüpfen.
Erstellen Sie unten in task_display.dart taskDisplay als Variable der obersten Ebene, verwenden Sie _TaskDisplayData, um das eingehende JSON zu parsen, und erstellen Sie eine Instanz des _TaskDisplay-Widgets.
final taskDisplay = CatalogItem(
name: 'TaskDisplay',
dataSchema: taskDisplaySchema,
widgetBuilder: (itemContext) {
final json = itemContext.data as Map<String, Object?>;
final data = _TaskDisplayData.fromJson(json);
return _TaskDisplay(
data: data,
onCompleteTask: (task) async {
// We will implement this next!
},
);
},
);
onCompleteTask implementieren
Damit das Widget funktioniert, muss es dem Agent mitteilen, wenn eine Aufgabe abgeschlossen ist. Ersetzen Sie den leeren Platzhalter onCompleteTask durch den folgenden Code, um ein Ereignis mit completeAction aus den Aufgabendaten zu erstellen und zu senden.
onCompleteTask: (task) async {
// A data context is a reference to a location in the data model. This line
// turns that reference into a concrete data object that the agent can use.
// It's kind of like taking a pointer and replacing it with the value it
// points to.
final JsonMap resolvedContext = await resolveContext(
itemContext.dataContext,
task.actionContext,
);
// Dispatch an event back to the agent, letting it know a task was completed.
// This will be sent to the agent in an A2UI message that includes the name
// of the action, the surface ID, and the resolved data context.
itemContext.dispatchEvent(
UserActionEvent(
name: task.actionName,
sourceComponentId: itemContext.id,
context: resolvedContext,
),
);
}
Katalogartikel registrieren
Öffnen Sie zuletzt main.dart, importieren Sie die neue Datei und registrieren Sie sie zusammen mit den anderen Katalogelementen.
Fügen Sie diesen Import oben in lib/main.dart ein:
import 'task_display.dart';
Ersetzen Sie catalog = BasicCatalogItems.asCatalog(); in Ihrer initState()-Funktion durch Folgendes:
// The Catalog is immutable, so use copyWith to create a new version
// that includes our custom catalog item along with the basics.
catalog = BasicCatalogItems.asCatalog().copyWith(newItems: [taskDisplay]);
Fertig! Führen Sie einen Hot-Restart der App durch, um die Änderungen zu sehen.
8. Mit verschiedenen Interaktionsmöglichkeiten mit dem Agenten experimentieren

Nachdem Sie das neue Widget dem Katalog hinzugefügt und in der Benutzeroberfläche der App Platz dafür geschaffen haben, können Sie mit dem Agenten arbeiten. Einer der Hauptvorteile von GenUI ist, dass Sie auf zwei Arten mit Ihren Daten interagieren können: über die Anwendungsoberfläche mit Schaltflächen und Kästchen sowie über einen Agenten, der natürliche Sprache versteht und die Daten analysieren kann. Probieren Sie beide aus.
- Beschreiben Sie im Textfeld drei oder vier Aufgaben. Sie werden dann in der Liste angezeigt.
- Mit einem Kästchen können Sie eine Aufgabe als erledigt oder nicht erledigt markieren.
- Erstellen Sie eine Liste mit 5 bis 6 Aufgaben und weisen Sie den Agent an, alle Aufgaben zu entfernen, für die Sie irgendwo hinfahren müssen.
- Bitten Sie den Agent, eine sich wiederholende Liste von Aufgaben als einzelne Elemente zu erstellen („Ich muss eine Feiertagskarte für Mama, Papa und Oma kaufen. Erstelle separate Aufgaben dafür.“).
- Weisen Sie den Agent an, alle Aufgaben als erledigt oder nicht erledigt zu markieren oder die ersten zwei oder drei Aufgaben abzuhaken.
9. Glückwunsch
Glückwunsch! Sie haben mit Generative UI und Flutter eine KI-basierte App zur Aufgabenverwaltung erstellt.
Das haben Sie gelernt
- Mit den Foundation Models von Google über das Flutter Firebase SDK interagieren
- Interaktive Oberflächen rendern, die von Gemini mit GenUI generiert wurden
- Oberflächen in Layouts mit vordefinierten statischen Rendering-IDs fixieren
- Benutzerdefinierte Schemas und Widget-Kataloge für robuste Interaktionsschleifen entwerfen