1. Giriş
Bu codelab'de Flutter, Firebase AI Logic ve yeni genui paketini kullanarak bir görev listesi uygulaması oluşturacaksınız. Metin tabanlı bir sohbet uygulamasıyla başlayacak, GenUI ile yükselterek temsilciye kendi kullanıcı arayüzünü oluşturma gücü verecek ve son olarak sizin ve temsilcinin doğrudan değiştirebileceği özel, etkileşimli bir kullanıcı arayüzü bileşeni oluşturacaksınız.

Yapacaklarınız
- Flutter ve Firebase AI Logic kullanarak temel bir sohbet arayüzü oluşturma
- Yapay zeka destekli yüzeyler oluşturmak için
genuipaketini entegre edin - Uygulama, temsilciden yanıt beklerken ilerleme çubuğu ekleme
- Adlandırılmış bir yüzey oluşturun ve bunu kullanıcı arayüzünde özel bir noktada gösterin.
- Görevlerin nasıl sunulacağı üzerinde kontrol sahibi olmanızı sağlayan özel bir GenUI katalog bileşeni oluşturun
İhtiyacınız olanlar
- Chrome gibi bir web tarayıcısı
- Yerel olarak yüklenmiş Flutter SDK
- Firebase CLI'nin yüklenmiş ve yapılandırılmış olması
Bu codelab, orta düzey Flutter geliştiricileri içindir.
2. Başlamadan önce
Flutter projesini ayarlama
Terminalinizi açın ve yeni bir proje oluşturmak için flutter create komutunu çalıştırın:
flutter create intro_to_genui
cd intro_to_genui
Flutter projenize gerekli bağımlılıkları ekleyin:
flutter pub add firebase_core firebase_ai genui json_schema_builder
Son dependencies bölümünüz aşağıdaki gibi görünmelidir (sürüm numaraları biraz farklı olabilir):
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
Tüm paketleri indirmek için flutter pub get komutunu çalıştırın.
API'leri ve Firebase'i etkinleştirme
firebase_ai paketini kullanmak için öncelikle projenizde Firebase AI Logic'i etkinleştirmeniz gerekir.
- Firebase konsolunda Firebase AI Logic'e gidin.
- Rehberli iş akışını başlatmak için Başlayın'ı tıklayın.
- Projenizi oluşturmak için ekrandaki talimatları uygulayın.
Daha fazla bilgi için Firebase'i Flutter uygulamasına ekleme talimatlarına göz atın.
API'ler etkinleştirildikten sonra FlutterFire CLI'yi kullanarak Flutter uygulamanızda Firebase'i başlatın:
flutterfire configure
Firebase projenizi seçin ve hedeflediğiniz platformlar (ör. Android, iOS, web) için yapılandırmak üzere istemleri uygulayın. Bu codelab, makinenize yalnızca Flutter SDK ve Chrome yüklenerek tamamlanabilir ancak uygulama diğer platformlarda da çalışır.
3. Temel bir sohbet arayüzü oluşturma
Üretken kullanıcı arayüzünü kullanabilmek için uygulamanızın bir temeli olmalıdır: Firebase AI Logic tarafından desteklenen, metin tabanlı temel bir sohbet uygulaması. Hızlıca başlamak için sohbet arayüzünün kurulumunu kopyalayıp yapıştırın.

Mesaj balonu widget'ı oluşturma
Kullanıcı ve temsilciden gelen kısa mesajları göstermek için uygulamanızda bir widget olması gerekir. lib/message_bubble.dart adlı yeni bir dosya oluşturun ve aşağıdaki sınıfı ekleyin:
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, tek bir sohbet mesajını gösteren bir StatelessWidget'dir. Bu widget, hem sizin hem de aracının mesajlarını göstermek için bu codelab'in ilerleyen bölümlerinde kullanılacak ancak çoğunlukla sadece şık bir Text widget'ıdır.
Chat kullanıcı arayüzünü main.dart içinde uygulama
lib/main.dart dosyasının tüm içeriğini aşağıdaki eksiksiz metin chatbot uygulamasıyla değiştirin:
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.
''';
Kopyalayıp yapıştırdığınız main.dart dosyası, Firebase AI Logic ve systemInstruction içindeki istemi kullanarak temel bir ChatSession oluşturur. TextItem öğelerinin listesini tutarak ve daha önce oluşturduğunuz MessageBubble widget'ını kullanarak bunları kullanıcı sorgularıyla birlikte göstererek etkileşim dönüşlerini yönetir.
Devam etmeden önce kontrol etmeniz gereken birkaç nokta:
initStateyöntemi, Firebase AI Logic ile bağlantının kurulduğu yerdir.- Uygulama,
TextFieldve temsilciye mesaj gönderme düğmesi sunar. _addMessageyöntemi, kullanıcının mesajının temsilciye gönderildiği yerdir.- Sohbet geçmişi,
_itemslistesinde saklanır. - Mesajlar,
MessageBubblewidget'ı kullanılarakListViewiçinde gösterilir.
Uygulamayı test etme
Bu işlem tamamlandıktan sonra uygulamayı çalıştırıp test edebilirsiniz.
flutter run -d chrome
Temsilciyle bugün tamamlamak istediğiniz görevler hakkında sohbet etmeyi deneyin. Tamamen metin tabanlı bir kullanıcı arayüzü işinizi görebilir ancak GenUI, deneyimi daha kolay ve hızlı hale getirebilir.
4. GenUI paketini entegre edin
Artık düz metinden üretken kullanıcı arayüzüne geçme zamanı. Temel Firebase mesajlaşma döngüsünü GenUI Conversation, Catalog ve SurfaceController nesneleriyle değiştireceksiniz. Bu sayede yapay zeka modeli, sohbet akışında gerçek Flutter widget'ları oluşturabilir.

genui paketi, bu codelab'de kullanacağınız beş sınıf sağlar:
SurfaceControllermodel tarafından oluşturulan kullanıcı arayüzü haritalarını ekrana yansıtır.A2uiTransportAdapterdahili GenUI isteklerini herhangi bir harici dil modeliyle köprüler.Conversation, Flutter uygulamanız için denetleyiciyi ve aktarım bağdaştırıcısını tek bir birleştirilmiş API ile sarmalar.Catalog, dil modelinin kullanabileceği widget'ları ve özellikleri açıklar.Surface, model tarafından oluşturulan kullanıcı arayüzünü gösteren bir widget'tır.
Oluşturulan Surface göstermeye hazırlanma
Mevcut kod, görüşmedeki tek bir kısa mesajı temsil eden bir TextItem sınıfı içeriyor. Aracı tarafından oluşturulan bir Surface öğesini temsil etmek için başka bir sınıf ekleyin:
class SurfaceItem extends ConversationItem {
final String surfaceId;
SurfaceItem({required this.surfaceId});
}
GenUI yapı taşlarını başlatma
lib/main.dart üst kısmında genui kitaplığını içe aktarın:
import 'package:genui/genui.dart' hide TextPart;
import 'package:genui/genui.dart' as genui;
Hem genui paketinde hem de firebase_ai paketinde TextPart sınıfı bulunur. genui öğesini bu şekilde içe aktararak TextPart sürümünü genui.TextPart olarak adlandırır ve ad çakışmasını önlersiniz.
_MyHomePageState içinde _chatSession sonrasında temel işlevsel denetleyicileri beyan edin:
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;
Ardından, GenUI kitaplığının denetleyicilerini hazırlamak için initState sürümünü güncelleyin.
Bu satırı initState alanından kaldırın:
_chatSession.sendMessage(Content.text(systemInstruction));
Ardından, aşağıdaki kodu ekleyin:
@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,
);
}
Bu kod, denetleyiciyi ve bağdaştırıcıyı yöneten bir Conversation cephesi oluşturur. Bu görüşme, uygulamanıza temsilcinin oluşturduğu içeriklere ayak uydurmak için kullanabileceği bir etkinlik akışı ve temsilciye mesaj gönderme yöntemi sunar.
Ardından, görüşme etkinlikleri için bir dinleyici oluşturun. Bunlar arasında yüzeyle ilgili etkinliklerin yanı sıra kısa mesajlar ve hatalarla ilgili etkinlikler de yer alır:
@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:
}
});
});
}
Son olarak, sistem istemini oluşturun ve temsilciye gönderin:
@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()),
);
}
Görüntüleme Yüzeyleri
Ardından, _items listesindeki SurfaceItem'leri göstermek için ListView öğesinin build yöntemini güncelleyin:
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,
),
),
},
],
),
),
Surface widget'ının oluşturucusu, hangi yüzeyin görüntülenmesinden sorumlu olduğunu belirten bir surfaceContext alır. Daha önce oluşturulan SurfaceController, _controller, her yüzeyin tanımını ve durumunu sağlar ve güncelleme olduğunda yeniden oluşturulmasını sağlar.
GenUI'yı Firebase AI Logic'e bağlama
genui paketi, "Kendi Modelini Getir" yaklaşımını kullanır. Bu yaklaşım, deneyiminize hangi LLM'nin güç vereceğini kontrol etmenizi sağlar. Bu durumda Firebase AI Logic'i kullanıyorsunuz ancak paket, çeşitli aracı ve sağlayıcılarla çalışacak şekilde oluşturulmuştur.
Bu özgürlük, biraz daha fazla sorumluluk getirir: genui paketi tarafından oluşturulan mesajları alıp seçtiğiniz aracıya göndermeniz ve aracının yanıtlarını alıp genui'ya geri göndermeniz gerekir.
Bunu yapmak için önceki adımda kodda referans verilen _sendAndReceive yöntemini tanımlayacaksınız. Bu kodu MyHomePageState cihazına ekleyin:
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!);
}
}
Bu yöntem, genui paketi tarafından temsilciye mesaj göndermesi gerektiğinde çağrılır. Yöntemin sonundaki addChunk çağrısı, aracının yanıtını genui paketine geri besleyerek yanıtın işlenmesini ve kullanıcı arayüzü oluşturulmasını sağlar.
Son olarak, mevcut _addMessage yönteminizi tamamen bu yeni sürümle değiştirin. Böylece iletiler doğrudan Firebase'e değil, Conversation'ye yönlendirilir:
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));
}
İşte bu kadar. Uygulamayı tekrar çalıştırmayı deneyin. Metin mesajlarının yanı sıra, düğmeler ve metin widget'ları gibi kullanıcı arayüzü yüzeyleri oluşturan aracı da görürsünüz.
Hatta temsilciden kullanıcı arayüzünü belirli bir şekilde göstermesini isteyebilirsiniz. Örneğin, "Görevlerimi her birini tamamlandı olarak işaretleme düğmesiyle birlikte sütun halinde göster" gibi bir mesaj yazmayı deneyin.
5. Bekleme durumu ekleme
LLM oluşturma işlemi eşzamansızdır. Yanıt beklenirken sohbet arayüzünün giriş düğmelerini devre dışı bırakması ve ilerleme göstergesi görüntülemesi gerekir. Böylece kullanıcı, GenUI'nın içerik oluşturduğunu anlar. Neyse ki genui paketi, görüşmenin durumunu izlemek için kullanabileceğiniz bir Listenable sağlar. Bu ConversationState değeri, modelin içerik oluşturup oluşturmadığını belirlemek için bir isWaiting özelliği içerir.
Giriş kontrollerini ValueListenableBuilder ile sarmalayın
ValueListenableBuilder oluşturun. Bu ValueListenableBuilder, lib/main.dart öğesinin alt kısmındaki Row öğesini (TextField ve ElevatedButton öğelerini içerir) sarmalar. Böylece _conversation.state öğesini dinleyebilirsiniz. state.isWaiting simgesini inceleyerek model içerik oluştururken girişi devre dışı bırakabilirsiniz.
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'),
),
],
);
},
),
İlerleme çubuğu ekleme
Ana Column widget'ını Stack içine alın ve LinearProgressIndicator öğesini bu yığının ikinci alt öğesi olarak ekleyin. Bu öğe, alta sabitlenir. İşiniz bittiğinde, Scaffold öğenizin body bölümü aşağıdaki gibi görünmelidir:
body: Stack( // New!
children: [
Column(
children: [
Expanded(
child: ListView(
controller: _scrollController,
padding: const EdgeInsets.all(16),
children: [
for (final item in _items)
switch (item) {
TextItem() => MessageBubble(
text: item.text,
isUser: item.isUser,
),
SurfaceItem() => Surface(
surfaceContext: _controller.contextFor(
item.surfaceId,
),
),
},
],
),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ValueListenableBuilder<ConversationState>(
valueListenable: _conversation.state,
builder: (context, state, child) {
return Row(
children: [
Expanded(
child: TextField(
controller: _textController,
onSubmitted:
state.isWaiting ? null : (_) => _addMessage(),
decoration: const InputDecoration(
hintText: 'Enter a message',
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: state.isWaiting ? null : _addMessage,
child: const Text('Send'),
),
],
);
},
),
),
),
],
),
// Listen to the state again, this time to render a progress indicator
ValueListenableBuilder<ConversationState>(
valueListenable: _conversation.state,
builder: (context, state, child) {
if (state.isWaiting) {
return const LinearProgressIndicator();
}
return const SizedBox.shrink();
},
),
],
),
6. GenUI yüzeyini kalıcı hale getirme
Şimdiye kadar görev listesi, kaydırılan sohbet akışında oluşturuluyordu. Her yeni mesaj veya yüzey, geldiği sırada listeye ekleniyordu. Sonraki adımda, bir yüzeye nasıl ad vereceğinizi ve bu yüzeyi kullanıcı arayüzünde belirli bir konumda nasıl göstereceğinizi öğreneceksiniz.
İlk olarak, main.dart üst kısmında, void main() öncesinde yüzey kimliği olarak kullanılacak bir sabit tanımlayın:
const taskDisplaySurfaceId = 'task_display';
İkinci olarak, bu kimliğe sahip yüzeylerin _items'ye eklenmemesi için Conversation işleyicisindeki switch ifadesini güncelleyin:
case ConversationSurfaceAdded added:
if (added.surfaceId != taskDisplaySurfaceId) {
_items.add(SurfaceItem(surfaceId: added.surfaceId));
_scrollToBottom();
}
Ardından, sohbet günlüğünüzün hemen üzerinde sabitlenmiş yüzey için bir alan oluşturmak üzere widget ağacınızın düzen yapısını açın. Bu iki widget'ı ana Column öğesinin ilk alt öğeleri olarak ekleyin:
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.topLeft,
child: Surface(
surfaceContext: _controller.contextFor(
taskDisplaySurfaceId,
),
),
),
),
const Divider(),
Şimdiye kadar temsilciniz, uygun gördüğü yüzeyleri oluşturma ve kullanma konusunda serbestti. Daha ayrıntılı talimatlar vermek için sistem istemini tekrar ziyaret etmeniz gerekir. ## USER INTERFACE bölümünü, systemInstruction sabitinde depolanan istemin sonuna ekleyin:
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.
''';
Aracınıza, kullanıcı arayüzü yüzeylerini ne zaman ve nasıl kullanacağı konusunda net talimatlar vermeniz önemlidir. Aracıya belirli bir katalog öğesini ve yüzey kimliğini kullanmasını (ve tek bir örneği yeniden kullanmasını) söyleyerek görmek istediğiniz arayüzü oluşturmasını sağlayabilirsiniz.
Yapılacak daha çok iş var ancak aracının, kullanıcı arayüzünün üst kısmında görev görüntüleme yüzeyini oluşturduğunu görmek için uygulamanızı tekrar çalıştırmayı deneyebilirsiniz.
7. Özel katalog widget'ınızı oluşturma
Bu aşamada TaskDisplay katalog öğesi mevcut değil. Sonraki birkaç adımda, veri şeması, bu şemayı ayrıştıracak bir sınıf, bir widget ve her şeyi bir araya getiren katalog öğesi oluşturarak bu sorunu düzelteceksiniz.
Öncelikle task_display.dart adlı bir dosya oluşturun ve aşağıdaki içe aktarma işlemlerini ekleyin:
import 'package:flutter/material.dart';
import 'package:genui/genui.dart';
import 'package:json_schema_builder/json_schema_builder.dart';
Veri şemasını oluşturma
Ardından, temsilcinin görev ekranı oluşturmak istediğinde sağlayacağı veri şemasını tanımlayın. Bu süreçte json_schema_builder paketindeki bazı gelişmiş oluşturucular kullanılır ancak temelde yalnızca aracıya gönderilen ve aracıdan gelen mesajlarda kullanılan bir JSON şeması tanımlarsınız.
Bileşen adına referans veren temel bir S.object ile başlayın:
final taskDisplaySchema = S.object(
properties: {
'component': S.string(enumValues: ['TaskDisplay']),
},
);
Ardından, title, tasks, name, isCompleted ve completeAction özelliklerini şema özelliklerine ekleyin.
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.',
),
},
),
),
},
);
completeAction özelliğine göz atın. Bu öğe, A2UI işlemini temsil eden bir şema özelliğinin oluşturucusu olan A2uiSchemas.action ile oluşturulur. Uygulama, şemaya bir işlem ekleyerek aslında aracıya "Hey, bana bir görev verdiğinde bu görevin tamamlandığını bildirmek için kullanabileceğim bir işlemin adını ve meta verilerini de sağla" diyor. Daha sonra, kullanıcı bir onay kutusuna dokunduğunda uygulama bu işlemi çağırır.
Ardından, şemaya required alanları ekleyin. Bunlar, temsilciye belirli özellikleri her seferinde doldurmasını söyler. Bu durumda her özellik gereklidir.
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'],
);
Veri ayrıştırma sınıfları oluşturma
Bu bileşenin örneklerini oluştururken aracı, şemayla eşleşen verileri gönderir. Gelen JSON'u kesin olarak türü belirlenmiş Dart nesnelerine ayrıştırmak için iki sınıf ekleyin. _TaskDisplayData'nın kök yapıyı nasıl işlediğine, iç dizi ayrıştırmayı ise _TaskData'ye nasıl devrettiğine dikkat edin.
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');
}
}
}
Daha önce Flutter ile geliştirme yaptıysanız bu sınıflar muhtemelen oluşturduklarınıza benzer. JsonMap kabul eder ve JSON'dan ayrıştırılan verileri içeren kesin türü belirlenmiş bir nesne döndürür.
_TaskData'deki actionName ve actionContext alanlarına göz atın. JSON'un completeAction özelliğinden çıkarılırlar ve işlemin adını ve veri bağlamını (işlemin GenUI'nın veri modelindeki konumuna yapılan bir referans) içerirler. Bunlar daha sonra UserActionEvent oluşturmak için kullanılacaktır.
Veri modeli, genui kitaplığı tarafından yönetilen, tüm dinamik kullanıcı arayüzü durumu için merkezi ve gözlemlenebilir bir depodur. Temsilci, katalogdan bir kullanıcı arayüzü bileşeni oluşturduğunda bileşenin şemasına uygun bir veri nesnesi de oluşturur. Bu veri nesnesi, istemcideki veri modelinde saklanır. Böylece, widget oluşturmak için kullanılabilir ve sonraki mesajlarda aracıya referans verilebilir (ör. completeAction widget'a bağlamak üzere olduğunuz).
Widget'ı ekleme
Şimdi listeyi görüntülemek için bir widget oluşturun. _TaskDisplayData sınıfının bir örneğini ve bir görev tamamlandığında çağrılacak geri çağırmayı kabul etmelidir.
class _TaskDisplay extends StatelessWidget {
final _TaskDisplayData data;
final void Function(_TaskData) onCompleteTask;
const _TaskDisplay({required this.data, required this.onCompleteTask});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
data.title,
style: Theme.of(context).textTheme.titleLarge,
),
),
...data.tasks.map(
(task) => CheckboxListTile(
title: Text(
task.name,
style: TextStyle(
decoration: task.isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
value: task.isCompleted,
onChanged: task.isCompleted
? null
: (val) {
if (val == true) {
onCompleteTask(task);
}
},
),
),
],
);
}
}
CatalogItem öğesini oluşturun
Şema, ayrıştırıcı ve widget oluşturulduktan sonra artık hepsini bir araya getirmek için bir CatalogItem oluşturabilirsiniz.
task_display.dart öğesinin en altında, üst düzey bir değişken olarak taskDisplay oluşturun, gelen JSON'u ayrıştırmak için _TaskDisplayData öğesini kullanın ve _TaskDisplay widget'ının bir örneğini oluşturun.
final taskDisplay = CatalogItem(
name: 'TaskDisplay',
dataSchema: taskDisplaySchema,
widgetBuilder: (itemContext) {
final json = itemContext.data as Map<String, Object?>;
final data = _TaskDisplayData.fromJson(json);
return _TaskDisplay(
data: data,
onCompleteTask: (task) async {
// We will implement this next!
},
);
},
);
onCompleteTask'ı uygulama
Widget'ın çalışması için bir görev tamamlandığında ajana geri bildirimde bulunması gerekir. Görev verilerindeki completeAction kullanılarak bir etkinlik oluşturup göndermek için boş onCompleteTask yer tutucusunu aşağıdaki kodla değiştirin.
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,
),
);
}
Katalog öğesini kaydetme
Son olarak, main.dart simgesini açın, yeni dosyayı içe aktarın ve diğer katalog öğeleriyle birlikte kaydedin.
Bu içe aktarma işlemini lib/main.dart dosyasının en üstüne ekleyin:
import 'task_display.dart';
initState() işlevinizdeki catalog = BasicCatalogItems.asCatalog(); yerine aşağıdakileri yazın:
// 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]);
Hepsi bu kadar! Değişiklikleri görmek için uygulamayı hızlıca yeniden başlatın.
8. Aracıyla etkileşim kurmanın farklı yollarını deneyin.

Kataloga yeni widget'ı eklediğinize ve uygulamanın kullanıcı arayüzünde bu widget için yer açtığınıza göre artık temsilciyle çalışırken eğlenme zamanı. GenUI'nın temel avantajlarından biri, verilerinizle etkileşim kurmanın iki yolunu sunmasıdır: düğmeler ve onay kutuları gibi uygulama kullanıcı arayüzü aracılığıyla ve doğal dili anlayıp veriler hakkında akıl yürütebilen bir aracı aracılığıyla. Her ikisiyle de deneme yapmayı deneyin.
- Metin alanını kullanarak üç veya dört görevi açıklayın ve bunların listede görünmesini izleyin.
- Bir görevi tamamlandı veya tamamlanmadı olarak değiştirmek için onay kutusunu kullanın.
- 5-6 görevden oluşan bir liste oluşturun ve temsilciye, bir yere gitmenizi gerektiren görevleri kaldırmasını söyleyin.
- Aracıya, görevlerin tekrarlanan bir listesini ayrı öğeler olarak oluşturmasını söyleyin ("Annem, babam ve büyükannem için bayram kartı almam gerekiyor. Bunlar için ayrı görevler oluşturun.").
- Ajana tüm görevleri tamamlandı veya tamamlanmadı olarak işaretlemesini ya da ilk iki veya üç görevi işaretlemesini söyleyin.
9. Tebrikler
Tebrikler! Üretken kullanıcı arayüzü ve Flutter'ı kullanarak yapay zeka destekli bir görev izleme uygulaması oluşturmuş olmanız gerekir.
Öğrendikleriniz
- Flutter Firebase SDK'sını kullanarak Google'ın temel modelleriyle etkileşimde bulunma
- GenUI kullanılarak Gemini tarafından oluşturulan etkileşimli yüzeyleri oluşturma
- Önceden belirlenmiş statik oluşturma kimliklerini kullanarak düzenlerde yüzeyleri sabitleme
- Güçlü etkileşim döngüleri için özel şemalar ve widget katalogları tasarlama