1. Introduzione
In questo codelab creerai un'app di elenco delle attività utilizzando Flutter, Firebase AI Logic e il nuovo pacchetto genui. Inizierai con un'app di chat basata su testo, la aggiornerai con GenUI per dare all'agente la possibilità di creare la propria UI e infine creerai il tuo componente UI personalizzato e interattivo che tu e l'agente potrete manipolare direttamente.

In questo lab proverai a:
- Crea un'interfaccia di chat di base utilizzando Flutter e Firebase AI Logic
- Integra il pacchetto
genuiper generare superfici basate sull'AI - Aggiungi una barra di avanzamento per indicare quando l'app è in attesa di una risposta dall'agente
- Crea una superficie denominata e mostrala in una posizione dedicata nell'interfaccia utente.
- Crea un componente del catalogo GenUI personalizzato che ti consenta di controllare la modalità di presentazione delle attività
Che cosa ti serve
- Un browser web, ad esempio Chrome
- L'SDK Flutter installato localmente
- L'interfaccia a riga di comando di Firebase installata e configurata
Questo codelab è destinato agli sviluppatori Flutter di livello intermedio.
2. Prima di iniziare
Configurare il progetto Flutter
Apri il terminale ed esegui flutter create per creare un nuovo progetto:
flutter create intro_to_genui
cd intro_to_genui
Aggiungi le dipendenze necessarie al tuo progetto Flutter:
flutter pub add firebase_core firebase_ai genui json_schema_builder
La sezione dependencies finale dovrebbe avere questo aspetto (i numeri di versione potrebbero variare leggermente):
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
Esegui flutter pub get per scaricare tutti i pacchetti.
Abilitare le API e Firebase
Per utilizzare il pacchetto firebase_ai, devi prima attivare Firebase AI Logic nel tuo progetto.
- Vai a Firebase AI Logic nella Console Firebase.
- Fai clic su Inizia per avviare il flusso di lavoro guidato.
- Segui le istruzioni sullo schermo per configurare il progetto.
Per saperne di più, consulta le istruzioni per aggiungere Firebase a un'app Flutter.
Una volta attive le API, inizializza Firebase nella tua app Flutter utilizzando l'interfaccia a riga di comando FlutterFire:
flutterfire configure
Seleziona il progetto Firebase e segui le istruzioni per configurarlo per le piattaforme di destinazione (ad esempio Android, iOS, web). Questo codelab può essere completato solo con l'SDK Flutter e Chrome installati sul tuo computer, ma l'app funzionerà anche su altre piattaforme.
3. Creare una struttura di base per un'interfaccia di chat
Prima di introdurre Generative UI, la tua app ha bisogno di una base: un'applicazione di chat di base basata su testo e su Firebase AI Logic. Per iniziare rapidamente, copia e incolla l'intera configurazione dell'interfaccia di chat.

Creare il widget della bolla del messaggio
Per visualizzare i messaggi dell'utente e dell'agente, la tua app ha bisogno di un widget. Crea un nuovo file denominato lib/message_bubble.dart e aggiungi la seguente classe:
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 è un StatelessWidget che mostra un singolo messaggio di chat. Verrà utilizzato più avanti in questo codelab per mostrare i messaggi tuoi e dell'agente, ma si tratta principalmente di un widget Text.
Implementare la UI di Chat in main.dart
Sostituisci l'intero contenuto di lib/main.dart con questa implementazione completa del chatbot di testo:
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.
''';
Il file main.dart che hai appena copiato e incollato configura un ChatSession di base utilizzando Firebase AI Logic e il prompt in systemInstruction. Gestisce i turni di conversazione mantenendo un elenco di elementi TextItem e visualizzandoli insieme alle query degli utenti utilizzando il widget MessageBubble che hai creato in precedenza.
Ecco alcuni aspetti da verificare prima di procedere:
- Il metodo
initStateè quello in cui viene configurata la connessione a Firebase AI Logic. - L'app offre un
TextFielde un pulsante per inviare messaggi all'agente. - Il metodo
_addMessageprevede l'invio del messaggio dell'utente all'agente. - L'elenco
_itemsè il luogo in cui viene memorizzata la cronologia delle conversazioni. - I messaggi vengono visualizzati in un
ListViewutilizzando il widgetMessageBubble.
Testare l'app
Ora puoi eseguire l'app e testarla.
flutter run -d chrome
Prova a chattare con l'agente per alcune attività che vorresti svolgere oggi. Anche se un'interfaccia utente basata esclusivamente su testo può svolgere il lavoro, GenUI può rendere l'esperienza più semplice e veloce.
4. Integra il pacchetto GenUI
Ora è il momento di passare dal testo normale all'interfaccia utente generativa. Sostituirai il ciclo di messaggistica Firebase di base con gli oggetti GenUI Conversation, Catalog e SurfaceController. Ciò consente al modello di AI di istanziare widget Flutter effettivi all'interno del flusso della chat.

Il pacchetto genui fornisce cinque classi che utilizzerai in questo codelab:
SurfaceControllerUI delle mappe generata dal modello sullo schermo.A2uiTransportAdaptercollega le richieste interne di GenUI a qualsiasi modello linguistico esterno.Conversationracchiude il controller e l'adattatore di trasporto con un'unica API unificata per la tua app Flutter.Catalogdescrive i widget e le proprietà disponibili per il modello linguistico.Surfaceè un widget che mostra l'interfaccia utente generata dal modello.
Preparati a visualizzare un Surface generato
Il codice esistente include una classe TextItem che rappresenta un singolo messaggio di testo all'interno della conversazione. Aggiungi un'altra classe per rappresentare un Surface creato dall'agente:
class SurfaceItem extends ConversationItem {
final String surfaceId;
SurfaceItem({required this.surfaceId});
}
Inizializzare i componenti di base di GenUI
Nella parte superiore di lib/main.dart, importa la libreria genui:
import 'package:genui/genui.dart' hide TextPart;
import 'package:genui/genui.dart' as genui;
Sia il pacchetto genui che il pacchetto firebase_ai includono una classe TextPart. Importando genui in questo modo, assegni uno spazio dei nomi alla sua versione di TextPart come genui.TextPart, evitando una collisione di nomi.
Dichiara i controller funzionali principali in _MyHomePageState dopo _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;
A questo punto, aggiorna initState per preparare i controller della libreria GenUI.
Rimuovi questa riga da initState:
_chatSession.sendMessage(Content.text(systemInstruction));
Quindi, aggiungi il seguente codice:
@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,
);
}
Questo codice crea una facciata Conversation che gestisce il controller e l'adattatore. Questa conversazione offre alla tua app un flusso di eventi che può utilizzare per tenere il passo con ciò che l'agente sta creando, nonché un metodo per inviare messaggi all'agente.
A questo punto, crea un listener per gli eventi di conversazione. Sono inclusi eventi relativi alla superficie, nonché quelli per messaggi ed errori:
@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:
}
});
});
}
Infine, crea il prompt di sistema e invialo all'agente:
@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()),
);
}
Superfici di visualizzazione
Successivamente, aggiorna il metodo build di ListView per visualizzare gli SurfaceItem nell'elenco _items:
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,
),
),
},
],
),
),
Il costruttore del widget Surface accetta un surfaceContext che indica la superficie che deve visualizzare. SurfaceController creato in precedenza, _controller, fornisce la definizione e lo stato di ogni superficie e garantisce la ricompilazione in caso di aggiornamento.
Collegare GenUI a Firebase AI Logic
Il pacchetto genui utilizza un approccio "Bring Your Own Model", il che significa che controlli quale LLM alimenta la tua esperienza. In questo caso, utilizzi Firebase AI Logic, ma il pacchetto è progettato per funzionare con una serie di agenti e fornitori.
Questa libertà comporta un po' di responsabilità in più: devi prendere i messaggi generati dal pacchetto genui e inviarli all'agente che hai scelto, e devi prendere le risposte dell'agente e inviarle di nuovo a genui.
Per farlo, definirai il metodo _sendAndReceive a cui viene fatto riferimento nel codice del passaggio precedente. Aggiungi questo codice a MyHomePageState:
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!);
}
}
Questo metodo verrà chiamato dal pacchetto genui ogni volta che deve inviare un messaggio all'agente. La chiamata a addChunk alla fine del metodo reinserisce la risposta dell'agente nel pacchetto genui, consentendogli di elaborare la risposta e generare la UI.
Infine, sostituisci completamente il metodo _addMessage esistente con questa nuova versione, in modo che i messaggi vengano indirizzati a Conversation anziché direttamente a Firebase:
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));
}
È tutto. Prova a eseguire di nuovo l'app. Oltre ai messaggi di testo, vedrai l'agente generare superfici UI come pulsanti, widget di testo e altro ancora.
Puoi anche chiedere all'agente di visualizzare la UI in un modo particolare. Ad esempio, prova a dire "Mostrami le mie attività in una colonna, con un pulsante per contrassegnare ognuna come completata".
5. Aggiungere lo stato di attesa
La generazione di LLM è asincrona. Durante l'attesa di una risposta, l'interfaccia della chat deve disattivare i pulsanti di input e mostrare un indicatore di avanzamento in modo che l'utente sappia che GenUI sta creando contenuti. Fortunatamente, il pacchetto genui fornisce un Listenable che puoi utilizzare per monitorare lo stato della conversazione. Il valore ConversationState include una proprietà isWaiting per determinare se il modello sta generando contenuti.
Racchiudi i controlli di input con un ValueListenableBuilder
Crea un ValueListenableBuilder che racchiuda il Row (che contiene TextField e ElevatedButton) nella parte inferiore di lib/main.dart per ascoltare _conversation.state. Se esamini state.isWaiting, puoi disattivare l'input mentre il modello genera contenuti.
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'),
),
],
);
},
),
Aggiungere una barra di avanzamento
Racchiudi il widget Column principale all'interno di un widget Stack e aggiungi LinearProgressIndicator come secondo elemento secondario dello stack, ancorato alla parte inferiore. Al termine, l'body del tuo Scaffold dovrebbe avere il seguente aspetto:
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. Persistenza di una superficie GenUI
Finora, l'elenco delle attività è stato visualizzato nel flusso della chat scorrevole, con ogni nuovo messaggio o superficie aggiunto all'elenco man mano che arriva. Nel passaggio successivo, vedrai come assegnare un nome a una superficie e visualizzarla in una posizione specifica all'interno dell'interfaccia utente.
Innanzitutto, nella parte superiore di main.dart, prima di void main(), dichiara una costante da utilizzare come ID superficie:
const taskDisplaySurfaceId = 'task_display';
In secondo luogo, aggiorna l'istruzione switch nel listener Conversation per assicurarti che qualsiasi superficie con questo ID non venga aggiunta a _items:
case ConversationSurfaceAdded added:
if (added.surfaceId != taskDisplaySurfaceId) {
_items.add(SurfaceItem(surfaceId: added.surfaceId));
_scrollToBottom();
}
Successivamente, apri la struttura del layout dell'albero dei widget per creare uno spazio per la superficie bloccata immediatamente sopra il log della chat. Aggiungi questi due widget come primi elementi secondari del Column principale:
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.topLeft,
child: Surface(
surfaceContext: _controller.contextFor(
taskDisplaySurfaceId,
),
),
),
),
const Divider(),
Finora, il tuo agente ha avuto carta bianca per creare e utilizzare le piattaforme come meglio credeva. Per fornire istruzioni più specifiche, devi rivedere il prompt di sistema. Aggiungi la seguente sezione ## USER INTERFACE alla fine del prompt memorizzato nella costante systemInstruction:
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.
''';
È importante fornire all'agente istruzioni chiare su quando e come utilizzare le superfici UI. Se indichi all'agente di utilizzare un elemento del catalogo e un ID superficie specifici (e di riutilizzare una singola istanza), puoi contribuire a garantire che crei l'interfaccia che vuoi visualizzare.
C'è ancora del lavoro da fare, ma puoi provare a eseguire di nuovo l'app per vedere l'agente creare la superficie di visualizzazione delle attività nella parte superiore della UI.
7. Creare il widget catalogo personalizzato
A questo punto, l'elemento del catalogo TaskDisplay non esiste. Nei prossimi passaggi, risolverai il problema creando uno schema dei dati, una classe per analizzare lo schema, un widget e l'elemento del catalogo che unisce tutto.
Innanzitutto, crea un file denominato task_display.dart e aggiungi le seguenti importazioni:
import 'package:flutter/material.dart';
import 'package:genui/genui.dart';
import 'package:json_schema_builder/json_schema_builder.dart';
Crea lo schema di dati
Successivamente, definisci lo schema dei dati che l'agente fornirà quando vuole creare una visualizzazione delle attività. Il processo utilizza alcuni costruttori avanzati del pacchetto json_schema_builder, ma essenzialmente stai solo definendo uno schema JSON utilizzato nei messaggi inviati e ricevuti dall'agente.
Inizia con un S.object di base che fa riferimento al nome del componente:
final taskDisplaySchema = S.object(
properties: {
'component': S.string(enumValues: ['TaskDisplay']),
},
);
Poi, aggiungi title, tasks, name, isCompleted e completeAction alle proprietà dello schema.
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.',
),
},
),
),
},
);
Dai un'occhiata alla proprietà completeAction. Viene creato con A2uiSchemas.action, il costruttore di una proprietà dello schema che rappresenta un'azione A2UI. Aggiungendo un'azione allo schema, l'app dice essenzialmente all'agente: "Ehi, quando mi dai un'attività, fornisci anche il nome e i metadati di un'azione che posso utilizzare per comunicarti che l'attività è stata completata". In un secondo momento, l'app richiamerà l'azione quando l'utente tocca una casella di controllo.
Successivamente, aggiungi i campi required allo schema. Queste istruzioni indicano all'agente di compilare sempre determinate proprietà. In questo caso, ogni proprietà è obbligatoria.
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'],
);
Crea classi di analisi dei dati
Quando crei istanze di questo componente, l'agente invia i dati corrispondenti allo schema. Aggiungi due classi per analizzare il JSON in entrata in oggetti Dart fortemente tipizzati. Nota come _TaskDisplayData gestisce la struttura principale, delegando l'analisi dell'array interno a _TaskData.
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');
}
}
}
Se hai già utilizzato Flutter, queste classi sono probabilmente simili a quelle che hai creato. Accettano un JsonMap e restituiscono un oggetto fortemente tipizzato contenente i dati analizzati da JSON.
Dai un'occhiata ai campi actionName e actionContext in _TaskData. Vengono estratti dalla proprietà completeAction del JSON e contengono il nome dell'azione e il relativo contesto dei dati (un riferimento alla posizione dell'azione nel modello di dati di GenUI). Verranno utilizzati in un secondo momento per creare un UserActionEvent.
Il modello di dati è un archivio centralizzato e osservabile per tutto lo stato dell'interfaccia utente dinamica, gestito dalla libreria genui. Quando l'agente crea un componente UI dal catalogo, crea anche un oggetto dati che corrisponde allo schema del componente. Questo oggetto dati viene memorizzato nel modello di dati nel client, in modo che possa essere utilizzato per creare widget e a cui si possa fare riferimento nei messaggi successivi all'agente (come completeAction che stai per collegare a un widget).
Aggiungi il widget
Ora crea un widget per visualizzare l'elenco. Deve accettare un'istanza della classe _TaskDisplayData e un callback da richiamare al completamento di un'attività.
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);
}
},
),
),
],
);
}
}
Crea CatalogItem
Con lo schema, il parser e il widget creati, ora puoi creare un CatalogItem per collegarli tutti.
Nella parte inferiore di task_display.dart, crea taskDisplay come variabile di primo livello, utilizza _TaskDisplayData per analizzare il JSON in entrata e crea un'istanza del widget _TaskDisplay.
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!
},
);
},
);
Implementare onCompleteTask
Affinché il widget funzioni, deve comunicare con l'agente quando un'attività viene completata. Sostituisci il segnaposto vuoto onCompleteTask con il seguente codice per creare e inviare un evento utilizzando completeAction dai dati dell'attività.
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,
),
);
}
Registra articolo del catalogo
Infine, apri main.dart, importa il nuovo file e registralo insieme agli altri elementi del catalogo.
Aggiungi questa importazione all'inizio di lib/main.dart:
import 'task_display.dart';
Sostituisci catalog = BasicCatalogItems.asCatalog(); nella funzione initState() con:
// 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]);
È tutto. Riavvia l'app per visualizzare le modifiche.
8. Sperimenta diversi modi di interagire con l'agente

Ora che hai aggiunto il nuovo widget al catalogo e hai creato uno spazio per lui nell'interfaccia utente dell'app, è il momento di divertirti a lavorare con l'agente. Uno dei principali vantaggi di GenUI è che offre due modi per interagire con i dati: tramite l'interfaccia utente dell'applicazione, ad esempio pulsanti e caselle di controllo, e tramite un agente che comprende il linguaggio naturale e può ragionare sui dati. Prova a sperimentare con entrambi.
- Usa il campo di testo per descrivere tre o quattro attività e vederle comparire nell'elenco.
- Utilizza una casella di controllo per contrassegnare un'attività come completata o non completata.
- Crea un elenco di 5-6 attività, poi chiedi all'agente di rimuovere quelle che richiedono di guidare da qualche parte.
- Chiedi all'agente di creare un elenco ripetitivo di attività come singoli elementi ("Devo comprare un biglietto di auguri per la festa della mamma, del papà e della nonna. Crea attività separate per queste cose").
- Chiedi all'agente di contrassegnare tutte le attività come completate o non completate oppure di spuntare le prime due o tre.
9. Complimenti
Complimenti! Hai creato un'app di monitoraggio delle attività basata sull'AI utilizzando Generative UI e Flutter.
Cosa hai imparato
- Interagire con i modelli di base di Google utilizzando l'SDK Firebase per Flutter
- Rendering di superfici interattive generate da Gemini utilizzando GenUI
- Bloccare le superfici nei layout utilizzando ID rendering statici predeterminati
- Progettazione di schemi personalizzati e cataloghi di widget per cicli di interazione solidi