1. Introdução
Neste codelab, você vai criar um app de lista de tarefas usando o Flutter, o Firebase AI Logic e o novo pacote genui. Você vai começar com um app de chat baseado em texto, fazer upgrade dele com a GenUI para dar ao agente o poder de criar a própria interface e, por fim, criar seu próprio componente de interface interativa e personalizada que você e o agente podem manipular diretamente.

Atividades deste laboratório
- Criar uma interface de chat básica usando o Flutter e o Firebase AI Logic
- Integrar o pacote
genuipara gerar interfaces com tecnologia de IA - Adicionar uma barra de progresso para indicar quando o app está aguardando uma resposta do agente
- Crie uma superfície nomeada e mostre-a em um local dedicado na interface.
- Crie um componente de catálogo GenUI personalizado que dê controle sobre como as tarefas são apresentadas
O que é necessário
- Um navegador da Web, como o Chrome
- O SDK do Flutter instalado localmente
- A CLI do Firebase instalada e configurada
Este codelab é destinado a desenvolvedores intermediários do Flutter.
2. Antes de começar
Configurar o projeto do Flutter
Abra o terminal e execute flutter create para criar um novo projeto:
flutter create intro_to_genui
cd intro_to_genui
Adicione as dependências necessárias ao projeto do Flutter:
flutter pub add firebase_core firebase_ai genui json_schema_builder
A seção dependencies final vai ficar assim (os números das versões podem variar um pouco):
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
Execute flutter pub get para baixar todos os pacotes.
Ativar APIs e o Firebase
Para usar o pacote firebase_ai, primeiro ative o Firebase AI Logic no seu projeto.
- Acesse a Lógica de IA do Firebase no console do Firebase.
- Clique em Começar para iniciar o fluxo de trabalho guiado.
- Siga as instruções na tela para configurar o projeto.
Para mais informações, confira as instruções para adicionar o Firebase a um app Flutter.
Depois que as APIs estiverem ativas, inicialize o Firebase no seu app Flutter usando a CLI do FlutterFire:
flutterfire configure
Selecione seu projeto do Firebase e siga as instruções para configurá-lo nas plataformas desejadas (por exemplo, Android, iOS, Web). Este codelab pode ser concluído apenas com o SDK do Flutter e o Chrome instalados na sua máquina, mas o app também vai funcionar em outras plataformas.
3. Criar uma estrutura para uma interface de chat básica
Antes de apresentar a interface generativa, seu app precisa de uma base: um aplicativo de chat básico baseado em texto com tecnologia da Firebase AI Logic. Para começar rapidamente, copie e cole toda a configuração da interface de chat.

Criar o widget de balão de mensagem
Para mostrar mensagens de texto do usuário e do agente, seu app precisa de um widget. Crie um arquivo chamado lib/message_bubble.dart e adicione a seguinte 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 é um StatelessWidget que mostra uma única mensagem de chat. Ele será usado mais adiante neste codelab para mostrar mensagens suas e do agente, mas é basicamente um widget Text sofisticado.
Implementar a interface do Chat em main.dart
Substitua todo o conteúdo de lib/main.dart por esta implementação completa do chatbot de texto:
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.
''';
O arquivo main.dart que você acabou de copiar e colar configura um ChatSession básico usando o Firebase AI Logic e o comando em systemInstruction. Ele gerencia as conversas mantendo uma lista de elementos TextItem e mostrando-os ao lado das consultas do usuário usando o widget MessageBubble que você criou antes.
Confira algumas coisas antes de continuar:
- O método
initStateé onde a conexão com o Firebase AI Logic é configurada. - O app oferece um
TextFielde um botão para enviar mensagens ao agente. - O método
_addMessageé onde a mensagem do usuário é enviada ao agente. - A lista
_itemsé onde o histórico de conversas é armazenado. - As mensagens são exibidas em um
ListViewusando o widgetMessageBubble.
Testar o app
Com isso, você pode executar e testar o app.
flutter run -d chrome
Converse com o agente sobre algumas tarefas que você quer realizar hoje. Embora uma interface puramente baseada em texto possa fazer o trabalho, a GenUI pode tornar a experiência mais fácil e rápida.
4. Integrar o pacote GenUI
Agora é hora de fazer upgrade do texto simples para a interface generativa. Você vai substituir o loop básico de mensagens do Firebase pelos objetos Conversation, Catalog e SurfaceController da GenUI. Isso permite que o modelo de IA crie widgets reais do Flutter no fluxo de chat.

O pacote genui oferece cinco classes que você vai usar ao longo deste codelab:
- O
SurfaceControllermapeia a interface do Google Maps gerada pelo modelo para a tela. - O
A2uiTransportAdapterconecta solicitações internas da GenUI com qualquer modelo de linguagem externo. - O
Conversationenvolve o controlador e o adaptador de transporte com uma única API unificada para seu app Flutter. Catalogdescreve os widgets e as propriedades disponíveis para o modelo de linguagem.Surfaceé um widget que mostra a interface gerada pelo modelo.
Prepare-se para mostrar um Surface gerado
O código atual inclui uma classe TextItem que representa uma única mensagem de texto na conversa. Adicione outra classe para representar um Surface criado pelo agente:
class SurfaceItem extends ConversationItem {
final String surfaceId;
SurfaceItem({required this.surfaceId});
}
Inicializar elementos básicos da GenUI
Na parte de cima de lib/main.dart, importe a biblioteca genui:
import 'package:genui/genui.dart' hide TextPart;
import 'package:genui/genui.dart' as genui;
Os pacotes genui e firebase_ai incluem uma classe TextPart. Ao importar genui dessa forma, você está criando um namespace para a versão de TextPart como genui.TextPart, evitando um conflito de nomes.
Declare os controladores funcionais principais em _MyHomePageState depois de _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;
Em seguida, atualize initState para preparar os controladores da biblioteca GenUI.
Remova esta linha de initState:
_chatSession.sendMessage(Content.text(systemInstruction));
Em seguida, adicione o seguinte código:
@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,
);
}
Esse código cria uma fachada Conversation que gerencia o controlador e o adaptador. Essa conversa oferece ao seu app um fluxo de eventos que ele pode usar para acompanhar o que o agente está criando, além de um método para enviar mensagens a ele.
Em seguida, crie um listener para eventos de conversa. Isso inclui eventos relacionados à superfície, bem como mensagens de texto e erros:
@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:
}
});
});
}
Por fim, crie o comando do sistema e envie para o 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()),
);
}
Plataformas de display
Em seguida, atualize o método build da ListView para mostrar os SurfaceItems na lista _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,
),
),
},
],
),
),
O construtor do widget Surface usa um surfaceContext que informa qual superfície ele é responsável por mostrar. O SurfaceController criado anteriormente, _controller, fornece a definição e o estado de cada superfície e garante que ele seja recriado quando houver uma atualização.
Conectar a GenUI ao Firebase AI Logic
O pacote genui usa uma abordagem "Traga seu próprio modelo", ou seja, você controla qual LLM impulsiona sua experiência. Neste caso, você está usando o Firebase AI Logic, mas o pacote foi criado para funcionar com vários agentes e provedores.
Essa liberdade resulta em um pouco mais de responsabilidade: você precisa pegar as mensagens geradas pelo pacote genui e enviá-las ao agente escolhido, além de pegar as respostas do agente e enviá-las de volta ao genui.
Para isso, defina o método _sendAndReceive referenciado no código da etapa anterior. Adicione este código 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!);
}
}
Esse método será chamado pelo pacote genui sempre que precisar enviar uma mensagem ao agente. A chamada para addChunk no final do método envia a resposta do agente de volta para o pacote genui, permitindo que ele processe a resposta e gere a interface.
Por fim, substitua o método _addMessage atual por esta nova versão para que ele encaminhe mensagens para o Conversation em vez do Firebase diretamente:
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));
}
Pronto! Tente executar o app novamente. Além de mensagens de texto, você vai ver o agente gerando interfaces, como botões, widgets de texto e muito mais.
Você pode até pedir para o agente mostrar a interface de um jeito específico. Por exemplo, tente uma mensagem como "Mostre minhas tarefas em uma coluna, com um botão para marcar cada uma como concluída".
5. Adicionar estado de espera
A geração de LLM é assíncrona. Enquanto aguarda uma resposta, a interface de chat precisa desativar os botões de entrada e mostrar um indicador de progresso para que o usuário saiba que a GenUI está criando conteúdo. Felizmente, o pacote genui fornece um Listenable que pode ser usado para rastrear o estado da conversa. Esse valor ConversationState inclui uma propriedade isWaiting para determinar se o modelo está gerando conteúdo.
Envolva os controles de entrada com um ValueListenableBuilder
Crie um ValueListenableBuilder que encapsule o Row (que contém TextField e ElevatedButton) na parte de baixo de lib/main.dart para detectar o _conversation.state. Ao inspecionar state.isWaiting, é possível desativar a entrada enquanto o modelo gera conteúdo.
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'),
),
],
);
},
),
Adicionar uma barra de progresso
Encapsule o widget Column principal em um Stack e adicione o LinearProgressIndicator como um segundo filho dessa pilha, ancorado na parte de baixo. Quando terminar, o body do seu Scaffold vai ficar assim:
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. Persistir uma superfície GenUI
Até agora, a lista de tarefas foi renderizada no stream de chat com rolagem, e cada nova mensagem ou superfície foi adicionada à lista conforme chegava. Na próxima etapa, você vai aprender a nomear uma superfície e mostrá-la em um local específico na interface.
Primeiro, na parte de cima de main.dart, antes de void main(), declare uma constante para usar como ID da superfície:
const taskDisplaySurfaceId = 'task_display';
Em seguida, atualize a instrução switch no listener Conversation para garantir que nenhuma superfície com esse ID seja adicionada a _items:
case ConversationSurfaceAdded added:
if (added.surfaceId != taskDisplaySurfaceId) {
_items.add(SurfaceItem(surfaceId: added.surfaceId));
_scrollToBottom();
}
Em seguida, abra a estrutura de layout da árvore de widgets para criar um espaço para a superfície fixada imediatamente acima do registro de chat. Adicione esses dois widgets como os primeiros filhos do Column principal:
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.topLeft,
child: Surface(
surfaceContext: _controller.contextFor(
taskDisplaySurfaceId,
),
),
),
),
const Divider(),
Até agora, seu agente teve liberdade para criar e usar superfícies como achou melhor. Para dar instruções mais específicas, é necessário revisar o comando do sistema. Adicione a seguinte seção ## USER INTERFACE ao final do comando armazenado na constante 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 dar instruções claras ao agente sobre quando e como usar as superfícies de interface. Ao pedir para o agente usar um item de catálogo e um ID de superfície específicos (e reutilizar uma única instância), você ajuda a garantir que ele crie a interface que você quer ver.
Ainda há mais trabalho a fazer, mas você pode tentar executar o app novamente para ver o agente criar a superfície de exibição de tarefas na parte de cima da interface.
7. Criar um widget de catálogo personalizado
Neste ponto, o item de catálogo TaskDisplay não existe. Nas próximas etapas, você vai corrigir isso criando um esquema de dados, uma classe para analisar esse esquema, um widget e o item de catálogo que junta tudo.
Primeiro, crie um arquivo chamado task_display.dart e adicione as seguintes importações:
import 'package:flutter/material.dart';
import 'package:genui/genui.dart';
import 'package:json_schema_builder/json_schema_builder.dart';
Criar o esquema de dados
Em seguida, defina o esquema de dados que o agente vai fornecer quando quiser criar uma exibição de tarefa. O processo usa alguns construtores sofisticados do pacote json_schema_builder, mas, essencialmente, você está apenas definindo um esquema JSON usado em mensagens para e do agente.
Comece com um S.object básico que faça referência ao nome do componente:
final taskDisplaySchema = S.object(
properties: {
'component': S.string(enumValues: ['TaskDisplay']),
},
);
Em seguida, adicione title, tasks, name, isCompleted e completeAction às propriedades do esquema.
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.',
),
},
),
),
},
);
Confira a propriedade completeAction. Ele é criado com A2uiSchemas.action, o construtor de uma propriedade de esquema que representa uma ação da A2UI. Ao adicionar uma ação ao esquema, o app está essencialmente dizendo ao agente: "Ei, quando você me der uma tarefa, também forneça o nome e os metadados de uma ação que posso usar para informar que a tarefa foi concluída". Mais tarde, o app vai invocar essa ação quando o usuário tocar em uma caixa de seleção.
Em seguida, adicione campos required ao esquema. Elas instruem o agente a preencher determinadas propriedades sempre. Nesse caso, todas as propriedades são obrigatórias.
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'],
);
Criar classes de análise de dados
Ao criar instâncias desse componente, o agente vai enviar dados que correspondem ao esquema. Adicione duas classes para analisar o JSON recebido em objetos Dart fortemente tipados. Observe como _TaskDisplayData processa a estrutura raiz, delegando a análise da matriz interna 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 você já criou com o Flutter, essas classes provavelmente são semelhantes às que você criou. Eles aceitam um JsonMap e retornam um objeto fortemente tipado que contém dados analisados de JSON.
Confira os campos actionName e actionContext em _TaskData. Eles são extraídos da propriedade completeAction do JSON e contêm o nome da ação e o contexto de dados dela (uma referência à localização da ação no modelo de dados da GenUI). Eles serão usados mais tarde para criar um UserActionEvent.
O modelo de dados é um armazenamento centralizado e observável para todo o estado dinâmico da interface, mantido pela biblioteca genui. Quando o agente cria um componente de interface do catálogo, ele também cria um objeto de dados que corresponde ao esquema do componente. Esse objeto de dados é armazenado no modelo de dados no cliente para que possa ser usado na criação de widgets e referenciado em mensagens posteriores ao agente (como o completeAction que você está prestes a conectar a um widget).
Adicionar o widget
Agora, crie um widget para mostrar a lista. Ele precisa aceitar uma instância da classe _TaskDisplayData e um callback para invocar quando uma tarefa for concluída.
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);
}
},
),
),
],
);
}
}
Criar o CatalogItem
Com o esquema, o analisador e o widget criados, agora é possível criar um CatalogItem para juntar tudo.
Na parte de baixo de task_display.dart, crie taskDisplay como uma variável de nível superior, use _TaskDisplayData para analisar o JSON recebido e crie uma instância do 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!
},
);
},
);
Implementar onCompleteTask
Para que o widget funcione, ele precisa se comunicar com o agente quando uma tarefa é concluída. Substitua o marcador de posição onCompleteTask vazio pelo código a seguir para criar e enviar um evento usando o completeAction dos dados da tarefa.
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,
),
);
}
Registrar item do catálogo
Por fim, abra main.dart, importe o novo arquivo e registre-o com os outros itens do catálogo.
Adicione esta importação à parte de cima de lib/main.dart:
import 'task_display.dart';
Substitua catalog = BasicCatalogItems.asCatalog(); na função initState() por:
// 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]);
Pronto! Faça uma reinicialização dinâmica do app para conferir as mudanças.
8. Teste diferentes maneiras de interagir com o agente

Agora que você adicionou o novo widget ao catálogo e criou um espaço para ele na interface do app, é hora de se divertir trabalhando com o agente. Um dos principais benefícios da GenUI é que ela oferece duas maneiras de interagir com seus dados: pela interface do aplicativo, como botões e caixas de seleção, e por um agente que entende a linguagem natural e pode analisar os dados. Teste os dois.
- Use o campo de texto para descrever três ou quatro tarefas e veja como elas aparecem na lista.
- Use uma caixa de seleção para marcar uma tarefa como concluída ou não concluída.
- Crie uma lista de cinco a seis tarefas e peça ao agente para remover as que exigem que você dirija até algum lugar.
- Peça ao agente para criar uma lista repetitiva de tarefas como itens individuais ("Preciso comprar um cartão de fim de ano para minha mãe, meu pai e minha avó. Crie tarefas separadas para isso").
- Diga ao agente para marcar todas as tarefas como concluídas ou não concluídas ou para marcar as duas ou três primeiras.
9. Parabéns
Parabéns! Você criou um app de acompanhamento de tarefas com tecnologia de IA usando a interface generativa e o Flutter.
O que você aprendeu
- Interagir com os modelos de base do Google usando o SDK do Firebase para Flutter
- Renderização de superfícies interativas geradas pelo Gemini usando a GenUI
- Fixação de superfícies em layouts usando IDs de renderização estáticos predeterminados
- Como criar esquemas personalizados e catálogos de widgets para loops de interação robustos