1. Pengantar
Dalam codelab ini, Anda akan membangun aplikasi daftar tugas menggunakan Flutter, Firebase AI Logic, dan paket genui baru. Anda akan memulai dengan aplikasi chat berbasis teks, mengupgradenya dengan GenUI untuk memberi agen kemampuan membuat UI-nya sendiri, dan terakhir membuat komponen UI interaktif kustom Anda sendiri yang dapat Anda dan agen manipulasi secara langsung.

Yang akan Anda lakukan
- Membangun antarmuka chat dasar menggunakan Flutter dan Firebase AI Logic
- Mengintegrasikan paket
genuiuntuk menghasilkan platform yang didukung AI - Menambahkan status progres untuk menunjukkan kapan aplikasi menunggu respons dari agen
- Buat permukaan bernama dan tampilkan di tempat khusus di UI.
- Buat komponen katalog GenUI kustom yang memberi Anda kontrol atas cara tugas ditampilkan
Yang Anda butuhkan
- Browser web, seperti Chrome
- Flutter SDK yang diinstal secara lokal
- Firebase CLI telah diinstal dan dikonfigurasi
Codelab ini ditujukan untuk developer Flutter tingkat menengah.
2. Sebelum memulai
Menyiapkan project Flutter
Buka terminal Anda dan jalankan flutter create untuk membuat project baru:
flutter create intro_to_genui
cd intro_to_genui
Tambahkan dependensi yang diperlukan ke project Flutter Anda:
flutter pub add firebase_core firebase_ai genui json_schema_builder
Bagian dependencies akhir Anda akan terlihat seperti ini (nomor versi mungkin sedikit berbeda):
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
Jalankan flutter pub get untuk mendownload semua paket.
Mengaktifkan API dan Firebase
Untuk menggunakan paket firebase_ai, Anda harus mengaktifkan Firebase AI Logic di project Anda terlebih dahulu.
- Buka Firebase AI Logic di Firebase console.
- Klik Mulai untuk meluncurkan alur kerja terpandu.
- Ikuti perintah di layar untuk menyiapkan project Anda.
Untuk mengetahui informasi selengkapnya, lihat petunjuk untuk menambahkan Firebase ke aplikasi Flutter.
Setelah API aktif, lakukan inisialisasi Firebase di aplikasi Flutter Anda menggunakan FlutterFire CLI:
flutterfire configure
Pilih project Firebase Anda dan ikuti perintah untuk mengonfigurasinya bagi platform target Anda (misalnya, Android, iOS, web). Codelab ini dapat diselesaikan hanya dengan Flutter SDK dan Chrome yang diinstal di perangkat Anda, tetapi aplikasi juga akan berfungsi di platform lain.
3. Membuat antarmuka chat dasar
Sebelum memperkenalkan UI Generatif, aplikasi Anda memerlukan fondasi: aplikasi chat berbasis teks dasar yang didukung oleh Firebase AI Logic. Untuk memulai dengan cepat, Anda akan menyalin dan menempelkan seluruh penyiapan untuk antarmuka chat.

Membuat widget balon pesan
Untuk menampilkan pesan teks dari pengguna dan agen, aplikasi Anda memerlukan widget. Buat file baru bernama lib/message_bubble.dart dan tambahkan class berikut:
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 adalah StatelessWidget yang menampilkan satu pesan chat. Widget ini akan digunakan nanti di codelab ini untuk menampilkan pesan dari Anda dan agen, tetapi sebagian besar hanya berupa widget Text yang menarik.
Menerapkan UI Chat di main.dart
Ganti seluruh konten lib/main.dart dengan implementasi chatbot teks lengkap ini:
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.
''';
File main.dart yang baru saja Anda salin dan tempel menyiapkan ChatSession dasar menggunakan Firebase AI Logic dan perintah di systemInstruction. Aplikasi ini mengelola giliran percakapan dengan mempertahankan daftar elemen TextItem dan menampilkannya bersama kueri pengguna menggunakan widget MessageBubble yang Anda buat sebelumnya.
Berikut beberapa hal yang perlu diperiksa sebelum melanjutkan:
- Metode
initStateadalah tempat penyiapan koneksi ke Firebase AI Logic. - Aplikasi menawarkan
TextFielddan tombol untuk mengirim pesan ke agen. - Metode
_addMessageadalah tempat pesan pengguna dikirim ke agen. - Daftar
_itemsadalah tempat histori percakapan disimpan. - Pesan ditampilkan dalam
ListViewmenggunakan widgetMessageBubble.
Menguji aplikasi
Dengan begitu, Anda dapat menjalankan aplikasi dan mengujinya.
flutter run -d chrome
Coba mulai percakapan dengan agen tentang beberapa tugas yang ingin Anda selesaikan hari ini. Meskipun UI berbasis teks murni dapat menyelesaikan tugas, GenUI dapat membuat pengalaman menjadi lebih mudah dan cepat.
4. Mengintegrasikan paket GenUI
Sekarang saatnya mengupgrade dari teks biasa ke UI Generatif. Anda akan menukar loop pesan Firebase dasar dengan objek GenUI Conversation, Catalog, dan SurfaceController. Hal ini memungkinkan model AI membuat instance widget Flutter yang sebenarnya dalam aliran chat.

Paket genui menyediakan lima class yang akan Anda gunakan di seluruh codelab ini:
SurfaceControllermemetakan UI yang dihasilkan oleh model ke layar.A2uiTransportAdaptermenjembatani permintaan GenUI internal dengan model bahasa eksternal.Conversationmembungkus pengontrol dan adaptor transportasi dengan satu API terpadu untuk aplikasi Flutter Anda.Catalogmenjelaskan widget dan properti yang tersedia untuk model bahasa.Surfaceadalah widget yang menampilkan UI yang dihasilkan oleh model.
Bersiap untuk menampilkan Surface yang dibuat
Kode yang ada mencakup class TextItem yang merepresentasikan satu pesan teks dalam percakapan. Tambahkan class lain untuk merepresentasikan Surface yang dibuat oleh agen:
class SurfaceItem extends ConversationItem {
final String surfaceId;
SurfaceItem({required this.surfaceId});
}
Menginisialisasi elemen penyusun GenUI
Di bagian atas lib/main.dart, impor library genui:
import 'package:genui/genui.dart' hide TextPart;
import 'package:genui/genui.dart' as genui;
Paket genui dan paket firebase_ai menyertakan class TextPart. Dengan mengimpor genui dengan cara ini, Anda akan memberi namespace pada versi TextPart sebagai genui.TextPart, sehingga menghindari konflik nama.
Deklarasikan pengontrol fungsional inti di _MyHomePageState setelah _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;
Selanjutnya, perbarui initState untuk menyiapkan pengontrol library GenUI.
Hapus baris ini dari initState:
_chatSession.sendMessage(Content.text(systemInstruction));
Kemudian, tambahkan kode berikut:
@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,
);
}
Kode ini membuat fasad Conversation yang mengelola pengontrol dan adaptor. Percakapan tersebut menawarkan aliran peristiwa yang dapat digunakan aplikasi Anda untuk mengikuti apa yang dibuat agen, serta metode untuk mengirim pesan ke agen.
Selanjutnya, buat pemroses untuk peristiwa percakapan. Peristiwa ini mencakup peristiwa terkait platform serta peristiwa untuk pesan teks dan error:
@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:
}
});
});
}
Terakhir, buat perintah sistem dan kirimkan ke agen:
@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()),
);
}
Platform Display
Selanjutnya, perbarui metode build ListView untuk menampilkan SurfaceItem di daftar _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,
),
),
},
],
),
),
Konstruktor untuk widget Surface mengambil surfaceContext yang memberi tahu permukaan mana yang menjadi tanggung jawabnya untuk ditampilkan. SurfaceController yang dibuat sebelumnya, _controller, memberikan definisi dan status untuk setiap platform, serta memastikan platform dibangun ulang saat ada update.
Menghubungkan GenUI ke Firebase AI Logic
Paket genui menggunakan pendekatan "Bawa Model Anda Sendiri", yang berarti Anda mengontrol LLM yang mendukung pengalaman Anda. Dalam hal ini, Anda menggunakan Firebase AI Logic, tetapi paket dibuat untuk berfungsi dengan berbagai agen dan penyedia.
Kebebasan tersebut menghasilkan sedikit tanggung jawab tambahan: Anda perlu mengambil pesan yang dihasilkan oleh paket genui dan mengirimkannya ke agen pilihan Anda, serta Anda perlu mengambil respons agen dan mengirimkannya kembali ke genui.
Untuk melakukannya, Anda akan menentukan metode _sendAndReceive yang direferensikan dalam kode untuk langkah sebelumnya. Tambahkan kode ini ke 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!);
}
}
Metode ini akan dipanggil oleh paket genui setiap kali perlu mengirim pesan ke agen. Panggilan ke addChunk di akhir metode akan memasukkan kembali respons agen ke dalam paket genui, sehingga paket tersebut dapat memproses respons dan membuat UI.
Terakhir, ganti metode _addMessage yang ada sepenuhnya dengan versi baru ini, sehingga pesan dirutekan ke Conversation, bukan langsung ke 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));
}
Selesai! Coba jalankan aplikasi lagi. Selain pesan teks, Anda akan melihat agen membuat platform UI seperti tombol, widget teks, dan lainnya.
Anda bahkan dapat mencoba meminta agen untuk menampilkan UI dengan cara tertentu. Misalnya, coba pesan seperti "Tampilkan tugas saya dalam kolom, dengan tombol untuk menandai setiap tugas sebagai selesai".
5. Menambahkan status menunggu
Pembuatan LLM bersifat asinkron. Saat menunggu respons, antarmuka chat perlu menonaktifkan tombol input dan menampilkan indikator progres agar pengguna tahu bahwa GenUI sedang membuat konten. Untungnya, paket genui menyediakan Listenable yang dapat Anda gunakan untuk melacak status percakapan. Nilai ConversationState tersebut mencakup properti isWaiting untuk menentukan apakah model sedang membuat konten.
Gabungkan kontrol input dengan ValueListenableBuilder
Buat ValueListenableBuilder yang membungkus Row (yang berisi TextField dan ElevatedButton) di bagian bawah lib/main.dart untuk memproses _conversation.state. Dengan memeriksa state.isWaiting, Anda dapat menonaktifkan input saat model membuat konten.
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'),
),
],
);
},
),
Menambahkan status progres
Bungkus widget Column utama di dalam Stack, dan tambahkan LinearProgressIndicator sebagai turunan kedua dari stack tersebut, yang di-anchor ke bagian bawah. Setelah selesai, body dari Scaffold Anda akan terlihat seperti ini:
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. Mempertahankan Permukaan GenUI
Sejauh ini, daftar tugas telah dirender dalam aliran chat yang dapat di-scroll, dengan setiap pesan atau platform baru ditambahkan ke daftar saat tiba. Pada langkah berikutnya, Anda akan melihat cara memberi nama platform dan menampilkannya di lokasi tertentu dalam UI.
Pertama, di bagian atas main.dart, sebelum void main(), deklarasikan konstanta untuk digunakan sebagai ID permukaan:
const taskDisplaySurfaceId = 'task_display';
Kedua, perbarui pernyataan switch di pemroses Conversation untuk memastikan bahwa platform dengan ID tersebut tidak ditambahkan ke _items:
case ConversationSurfaceAdded added:
if (added.surfaceId != taskDisplaySurfaceId) {
_items.add(SurfaceItem(surfaceId: added.surfaceId));
_scrollToBottom();
}
Selanjutnya, buka struktur tata letak hierarki widget Anda untuk membuat ruang bagi permukaan yang disematkan tepat di atas log chat Anda. Tambahkan kedua widget ini sebagai turunan pertama dari Column utama:
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.topLeft,
child: Surface(
surfaceContext: _controller.contextFor(
taskDisplaySurfaceId,
),
),
),
),
const Divider(),
Sejauh ini, agen Anda memiliki kebebasan untuk membuat dan menggunakan platform sesuai keinginannya. Untuk memberikan petunjuk yang lebih spesifik, Anda harus membuka kembali perintah sistem. Tambahkan bagian ## USER INTERFACE berikut ke akhir perintah yang disimpan dalam konstanta 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.
''';
Penting untuk memberikan petunjuk yang jelas kepada agen Anda tentang kapan dan cara menggunakan permukaan UI. Dengan memberi tahu agen untuk menggunakan item katalog dan ID platform tertentu (serta menggunakan kembali satu instance), Anda dapat membantu memastikan agen membuat antarmuka yang ingin Anda lihat.
Masih ada tugas lain yang perlu dilakukan, tetapi Anda dapat mencoba menjalankan aplikasi lagi untuk melihat agen membuat permukaan tampilan tugas di bagian atas UI.
7. Membuat widget katalog kustom
Pada tahap ini, item katalog TaskDisplay tidak ada. Dalam beberapa langkah berikutnya, Anda akan memperbaikinya dengan membuat skema data, class untuk mengurai skema tersebut, widget, dan item katalog yang menggabungkan semuanya.
Pertama, buat file bernama task_display.dart dan tambahkan impor berikut:
import 'package:flutter/material.dart';
import 'package:genui/genui.dart';
import 'package:json_schema_builder/json_schema_builder.dart';
Membuat skema data
Selanjutnya, tentukan skema data yang akan disediakan agen saat ingin membuat tampilan tugas. Proses ini menggunakan beberapa konstruktor canggih dari paket json_schema_builder, tetapi pada dasarnya Anda hanya menentukan skema JSON yang digunakan dalam pesan ke dan dari agen.
Mulai dengan S.object dasar yang mereferensikan nama komponen:
final taskDisplaySchema = S.object(
properties: {
'component': S.string(enumValues: ['TaskDisplay']),
},
);
Selanjutnya, tambahkan title, tasks, name, isCompleted, dan completeAction ke properti 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.',
),
},
),
),
},
);
Lihat properti completeAction. Objek ini dibuat dengan A2uiSchemas.action, konstruktor untuk properti skema yang merepresentasikan Tindakan A2UI. Dengan menambahkan tindakan ke skema, aplikasi pada dasarnya memberi tahu agen, "Hei, saat Anda memberi saya tugas, berikan juga nama dan metadata untuk tindakan yang dapat saya gunakan untuk memberi tahu Anda bahwa tugas tersebut telah selesai." Selanjutnya, aplikasi akan memanggil tindakan tersebut saat pengguna mengetuk kotak centang.
Selanjutnya, tambahkan kolom required ke skema. Petunjuk ini menginstruksikan agen untuk mengisi properti tertentu setiap saat. Dalam hal ini, setiap properti diperlukan.
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'],
);
Membuat class parsing data
Saat membuat instance komponen ini, agen akan mengirimkan data yang cocok dengan skema. Tambahkan dua class untuk mengurai JSON masuk tersebut ke dalam objek Dart yang memiliki jenis yang ditentukan secara ketat. Perhatikan cara _TaskDisplayData menangani struktur root, sekaligus mendelegasikan penguraian array dalam ke _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');
}
}
}
Jika Anda pernah membangun dengan Flutter sebelumnya, class ini mungkin mirip dengan class yang pernah Anda buat. Fungsi ini menerima JsonMap dan menampilkan objek berjenis kuat yang berisi data yang diuraikan dari JSON.
Lihat kolom actionName dan actionContext di _TaskData. Data ini diekstrak dari properti completeAction JSON dan berisi nama tindakan serta konteks datanya (referensi ke lokasi tindakan dalam model data GenUI). Variabel ini akan digunakan nanti untuk membuat UserActionEvent.
Model data adalah penyimpanan terpusat yang dapat diamati untuk semua status UI dinamis, yang dikelola oleh library genui. Saat membuat komponen UI dari katalog, agen juga membuat objek data yang cocok dengan skema komponen. Objek data ini disimpan dalam model data di klien, sehingga dapat digunakan untuk membuat widget dan direferensikan dalam pesan berikutnya ke agen (seperti completeAction yang akan Anda hubungkan ke widget).
Menambahkan widget
Sekarang, buat widget untuk menampilkan daftar. Metode ini harus menerima instance class _TaskDisplayData dan callback yang akan dipanggil saat tugas selesai.
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);
}
},
),
),
],
);
}
}
Buat CatalogItem
Setelah skema, parser, dan widget dibuat, Anda kini dapat membuat CatalogItem untuk mengikat semuanya.
Di bagian bawah task_display.dart, buat taskDisplay sebagai variabel tingkat teratas, gunakan _TaskDisplayData untuk mengurai JSON yang masuk, dan buat instance 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!
},
);
},
);
Menerapkan onCompleteTask
Agar widget berfungsi, widget perlu berkomunikasi kembali dengan agen saat tugas selesai. Ganti placeholder onCompleteTask yang kosong dengan kode berikut untuk membuat dan mengirimkan peristiwa menggunakan completeAction dari data tugas.
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,
),
);
}
Mendaftarkan item katalog
Terakhir, buka main.dart, impor file baru, dan daftarkan bersama item katalog lainnya.
Tambahkan impor ini ke bagian atas lib/main.dart:
import 'task_display.dart';
Ganti catalog = BasicCatalogItems.asCatalog(); di fungsi initState() dengan:
// 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]);
Selesai! Mulai ulang aplikasi dengan cepat untuk melihat perubahan.
8. Bereksperimen dengan berbagai cara untuk berinteraksi dengan agen

Setelah menambahkan widget baru ke Katalog dan menyediakan ruang untuknya di UI aplikasi, saatnya bersenang-senang menggunakan agen. Salah satu manfaat utama GenUI adalah menawarkan dua cara untuk berinteraksi dengan data Anda: melalui UI aplikasi seperti tombol dan kotak centang, serta melalui agen yang memahami bahasa alami dan dapat menalar data. Cobalah bereksperimen dengan keduanya.
- Gunakan kolom teks untuk mendeskripsikan tiga atau empat tugas, dan lihat tugas tersebut muncul dalam daftar.
- Gunakan kotak centang untuk mengubah status tugas menjadi selesai atau belum selesai.
- Buat daftar 5-6 tugas, lalu minta agen untuk menghapus tugas yang mengharuskan Anda berkendara ke suatu tempat.
- Minta agen untuk membuat daftar tugas berulang sebagai item individual ("Saya perlu membeli kartu ucapan untuk Ibu, Ayah, dan Nenek. Buat tugas terpisah untuk itu.").
- Minta agen untuk menandai semua tugas sebagai selesai atau belum selesai, atau untuk mencentang dua atau tiga tugas pertama.
9. Selamat
Selamat! Anda telah membuat aplikasi pelacakan tugas yang didukung AI menggunakan Generative UI dan Flutter.
Yang telah Anda pelajari
- Berinteraksi dengan model dasar Google menggunakan Flutter Firebase SDK
- Merender permukaan interaktif yang dibuat oleh Gemini menggunakan GenUI
- Menyematkan permukaan dalam tata letak menggunakan ID rendering statis yang telah ditentukan sebelumnya
- Mendesain skema kustom dan katalog widget untuk loop interaksi yang andal