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 hem sizin hem de temsilcinin doğrudan değiştirebileceği özel ve 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 alanda 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
- Henüz yapmadıysanız Flutter SDK'yı yerel olarak yükleyin.
- Terminalinizi açın ve yeni bir proje oluşturmak için
flutter createkomutunu çalıştırın:flutter create intro_to_genui cd intro_to_genui - Flutter projenize gerekli bağımlılıkları ekleyin:
Sonflutter pub add firebase_core firebase_ai genui json_schema_builderdependenciesbölümünüz aşağıdaki gibi görünmelidir (sürüm numaraları biraz farklılık gösterebilir):dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.8 firebase_core: ^4.9.0 firebase_ai: ^3.12.1 genui: ^0.9.0 json_schema_builder: ^0.1.3 - Tüm paketleri indirmek için
flutter pub getkomutunu çalıştırın.
Firebase projesi oluşturma
- Henüz yapmadıysanız Firebase CLI'yi yükleyin.
- Google Hesabınızı kullanarak Firebase'e giriş yapın:
firebase login - FlutterFire CLI'yı yükleyin:
dart pub global activate flutterfire_cli - Flutter projenizi Firebase kullanacak şekilde yapılandırmak için Flutter proje dizininizden aşağıdaki komutu çalıştırın:
Bu komut şunları yapar:flutterfire configure- Mevcut bir Firebase projesini kullanmak mı yoksa yeni bir Firebase projesi oluşturmak mı istediğinizi sorar. Yeni bir Firebase projesi oluştur'u seçin.
- Flutter uygulamanız için hangi platformu (iOS, Android, Web) hedeflemek istediğinizi sorar. Şimdilik Web'i seçin.
flutterfire configure komutu, otomatik olarak bir Firebase projesi ve bu Firebase projesinde yeni bir Firebase web uygulaması oluşturur. Ardından komut, bir Firebase yapılandırma dosyası (firebase_options.dart) oluşturur ve bu dosyayı Flutter projenizin lib/ dizinine otomatik olarak ekler.
Bu codelab'deki uygulamanın yalnızca makinenize yüklenen Flutter SDK ve Chrome ile çalıştığını (yani web uygulaması olarak oluşturulduğunu) unutmayın. Ancak bu uygulama Flutter ile geliştirildiği için diğer platformlarda da çalışır. Bu nedenle, codelab'in sonunda iOS, Android veya başka bir platform için destek eklemek üzere flutterfire configure komutunu yeniden çalıştırmayı ve ardından uygulamayı bu platformda yeniden oluşturmayı deneyin.
Daha fazla bilgi için Firebase'i Flutter uygulamasına ekleme talimatlarına göz atın.
Firebase AI Logic'i ayarlama
- Firebase konsolunda oturum açın. Firebase CLI'da oturum açmak için kullandığınız Google Hesabı'nı kullanın.
- FlutterFire CLI ile yeni oluşturduğunuz Firebase projesini seçin.
- Soldaki gezinme menüsünden AI Hizmetleri > AI Logic'i seçin.
- Rehberli iş akışını başlatmak için Başlayın'ı tıklayın.
- Gemini Developer API ile başlamak için seçeneği belirleyin ve Firebase AI Logic'i ayarlamak için ekrandaki talimatları uygulayın.
"Flutter projesi oluşturma" bölümünde Firebase AI Logic'i kullanmak için gereken FlutterFire eklentileri zaten mevcuttur. Sonraki adımda uygulamanızı kodlamaya başlayabilirsiniz.
3. Temel bir sohbet arayüzü oluşturma
Üretken kullanıcı arayüzünü kullanıma sunmadan önce 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'ını 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 temsilcinin 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'da 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.5-flash',
);
_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:
- Firebase AI Logic ile bağlantının kurulduğu yöntem
initStateyöntemidir. - Uygulamada
TextFieldsimgesi ve temsilciye mesaj gönderme düğmesi bulunur. _addMessageyöntemi, kullanıcının mesajının temsilciye gönderildiği yöntemdir.- Sohbet geçmişi
_itemslistesinde saklanır. - Mesajlar,
ListViewiçindeMessageBubblewidget'ı kullanılarak gösterilir.
Uygulamayı test etme
Bu işlem tamamlandığında uygulamayı çalıştırıp test edebilirsiniz.
flutter run -d chrome
Temsilciyle bugün yapmak 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 haritalar kullanıcı arayüzü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. Temsilci 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 öğesinin 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 cephe 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 olanlar da 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 aracıya 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 öğelerini 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üzeyi görüntülemekten 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 edebileceğiniz anlamına gelir. Bu örnekte 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 ajana 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şlenmesine ve kullanıcı arayüzü oluşturulmasına olanak tanır.
Son olarak, mevcut _addMessage yönteminizi tamamen bu yeni sürümle değiştirin. Böylece iletiler doğrudan Firebase yerine 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, metin widget'ları gibi kullanıcı arayüzü öğelerini oluşturan temsilciyi de 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
_conversation.state dinlemek için lib/main.dart alt kısmındaki Row (TextField ve ElevatedButton içeren) öğesini sarmalayan bir ValueListenableBuilder oluşturun. 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 bildirin:
const taskDisplaySurfaceId = 'task_display';
İkinci olarak, bu kimliğe sahip yüzeylerin _items'ye eklenmemesi için Conversation dinleyicisindeki 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üğü gibi yüzeyler oluşturma ve kullanma konusunda serbestti. Daha spesifik talimatlar vermek için sistem istemini tekrar ziyaret etmeniz gerekir. ## USER INTERFACE sabitinde depolanan istemin sonuna aşağıdaki ## USER INTERFACE bölümünü ekleyin: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.
''';
Aracınıza, kullanıcı arayüzü yüzeylerini ne zaman ve nasıl kullanacağıyla ilgili 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ına yardımcı olabilirsiniz.
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üzeyi oluşturmasını 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 işlemde 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, A2UI İşlemi'ni 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 uygulama, kullanıcı bir onay kutusuna dokunduğunda bu işlemi çağırır.
Ardından, şemaya required alanları ekleyin. Bunlar, ajana belirli özellikleri her seferinde doldurması talimatını verir. 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'ı kesin 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 olarak 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. Ajan, 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 iletilerde aracıya (ör. bir widget'a bağlamak üzere olduğunuz completeAction) referans verilebilir.
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 kullanarak 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 tıklayarak 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 şunu 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. Ajanla etkileşim kurmanın farklı yollarını deneyin.

Yeni widget'ı kataloga 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'nin 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 tekrar eden 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şim kurma
- GenUI'dan yararlanarak 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