۱. یک برنامه Flutter مبتنی بر Gemini بسازید
آنچه خواهید ساخت
در این آزمایشگاه کد، شما Colorist را خواهید ساخت - یک برنامه تعاملی Flutter که قدرت Gemini API را مستقیماً به برنامه Flutter شما میآورد. آیا تا به حال خواستهاید که به کاربران اجازه دهید برنامه شما را از طریق زبان طبیعی کنترل کنند اما نمیدانید از کجا شروع کنید؟ این آزمایشگاه کد به شما نشان میدهد که چگونه.
Colorist به کاربران اجازه میدهد رنگها را به زبان طبیعی توصیف کنند (مانند «رنگ نارنجی غروب خورشید» یا «آبی عمیق اقیانوس») و برنامه:
- این توضیحات را با استفاده از رابط برنامهنویسی کاربردی گوگل (Gemini API) پردازش میکند.
- توضیحات را به مقادیر رنگی دقیق RGB تفسیر میکند
- نمایش رنگ روی صفحه نمایش به صورت زنده
- جزئیات فنی رنگ و زمینه جالبی در مورد رنگ ارائه میدهد
- تاریخچه رنگهای اخیراً تولید شده را نگه میدارد

این برنامه دارای یک رابط کاربری دو صفحهای با یک ناحیه نمایش رنگی و یک سیستم چت تعاملی در یک طرف و یک پنل گزارش دقیق است که تعاملات خام LLM را در طرف دیگر نشان میدهد. این گزارش به شما امکان میدهد تا بهتر بفهمید که چگونه یک ادغام LLM در زیر کاپوت کار میکند.
چرا این موضوع برای توسعهدهندگان فلاتر اهمیت دارد؟
دورههای LLM نحوه تعامل کاربران با برنامهها را متحول میکنند، اما ادغام مؤثر آنها در برنامههای تلفن همراه و دسکتاپ چالشهای منحصر به فردی را به همراه دارد. این آزمایشگاه کد، الگوهای عملی را به شما آموزش میدهد که فراتر از فراخوانیهای خام API هستند.
سفر یادگیری شما
این آزمایشگاه کد، مراحل ساخت Colorist را گام به گام طی میکند:
- راهاندازی پروژه - شما با یک ساختار اولیه برنامه Flutter و پکیج
colorist_uiشروع خواهید کرد. - ادغام اولیه Gemini - برنامه خود را به Firebase AI Logic متصل کنید و ارتباط LLM را پیادهسازی کنید
- راهنمایی مؤثر - یک راهنمای سیستمی ایجاد کنید که LLM را برای درک توضیحات رنگ راهنمایی کند
- اعلانهای تابع - ابزارهایی را تعریف کنید که LLM میتواند برای تنظیم رنگها در برنامه شما استفاده کند
- مدیریت ابزار - فراخوانیهای تابع را از LLM پردازش کرده و آنها را به وضعیت برنامه خود متصل کنید
- پاسخهای استریمینگ - تجربه کاربری را با پاسخهای LLM استریمینگ بلادرنگ بهبود بخشید
- همگامسازی زمینه LLM - با آگاه کردن LLM از اقدامات کاربر، یک تجربه منسجم ایجاد کنید
آنچه یاد خواهید گرفت
- پیکربندی منطق هوش مصنوعی فایربیس برای برنامههای فلاتر
- ایجاد دستورالعملهای سیستمی مؤثر برای هدایت رفتار LLM
- پیادهسازی اعلانهای تابع که زبان طبیعی و ویژگیهای برنامه را به هم متصل میکنند
- پردازش پاسخهای جاری برای یک تجربه کاربری واکنشگرا
- همگامسازی وضعیت بین رویدادهای رابط کاربری و LLM
- مدیریت وضعیت مکالمه LLM با استفاده از Riverpod
- مدیریت خطاها به طور مناسب در برنامههای مبتنی بر LLM
پیشنمایش کد: نگاهی اجمالی به آنچه پیادهسازی خواهید کرد
در اینجا نگاهی اجمالی به تعریف تابعی که برای تنظیم رنگها در برنامهتان توسط LLM ایجاد خواهید کرد، میاندازیم:
FunctionDeclaration get setColorFuncDecl => FunctionDeclaration(
'set_color',
'Set the color of the display square based on red, green, and blue values.',
parameters: {
'red': Schema.number(description: 'Red component value (0.0 - 1.0)'),
'green': Schema.number(description: 'Green component value (0.0 - 1.0)'),
'blue': Schema.number(description: 'Blue component value (0.0 - 1.0)'),
},
);
مروری ویدیویی بر این آزمایشگاه کد
بحث کریگ لابنز و اندرو بروگدون در مورد این آزمایشگاه کد را در بخش تشریح شماره ۵۹ از Observable Flutter تماشا کنید:
پیشنیازها
برای اینکه بیشترین بهره را از این آزمایشگاه کد ببرید، باید موارد زیر را داشته باشید:
- تجربه توسعه Flutter - آشنایی با اصول اولیه Flutter و نحو Dart
- دانش برنامهنویسی ناهمزمان - آشنایی با مفاهیم Futures، async/await و Streams
- حساب کاربری فایربیس - برای راهاندازی فایربیس به یک حساب کاربری گوگل نیاز دارید
بیایید ساخت اولین برنامه Flutter مبتنی بر LLM شما را شروع کنیم!
۲. راهاندازی پروژه و سرویس اکو
در این مرحله اول، ساختار پروژه را تنظیم کرده و یک سرویس echo پیادهسازی خواهید کرد که بعداً با ادغام API Gemini جایگزین خواهد شد. این کار معماری برنامه را ایجاد کرده و قبل از اضافه کردن پیچیدگی فراخوانیهای LLM، عملکرد صحیح رابط کاربری شما را تضمین میکند.
آنچه در این مرحله خواهید آموخت
- راهاندازی یک پروژه Flutter با وابستگیهای مورد نیاز
- کار با پکیج
colorist_uiبرای کامپوننتهای رابط کاربری - پیادهسازی سرویس پیام اکو و اتصال آن به رابط کاربری
ایجاد یک پروژه جدید فلاتر
با ایجاد یک پروژه جدید Flutter با دستور زیر شروع کنید:
flutter create -e colorist --platforms=android,ios,macos,web,windows
علامت -e نشان میدهد که شما یک پروژه خالی بدون برنامه پیشفرض counter میخواهید. این برنامه طوری طراحی شده است که در دسکتاپ، موبایل و وب کار کند. با این حال، flutterfire در حال حاضر از لینوکس پشتیبانی نمیکند.
وابستگیها را اضافه کنید
به دایرکتوری پروژه خود بروید و وابستگیهای مورد نیاز را اضافه کنید:
cd colorist
flutter pub add colorist_ui flutter_riverpod riverpod_annotation
flutter pub add --dev build_runner riverpod_generator riverpod_lint json_serializable custom_lint
این کار بستههای کلیدی زیر را اضافه میکند:
-
colorist_ui: یک بسته سفارشی که اجزای رابط کاربری را برای برنامه Colorist فراهم میکند. -
flutter_riverpodوriverpod_annotation: برای مدیریت وضعیت (state management) -
logging: برای ثبت وقایع ساختاریافته - وابستگیهای توسعه برای تولید کد و linting
pubspec.yaml شما چیزی شبیه به این خواهد بود:
pubspec.yaml
name: colorist
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0
environment:
sdk: ^3.9.2
dependencies:
flutter:
sdk: flutter
colorist_ui: ^0.3.0
flutter_riverpod: ^3.0.0
riverpod_annotation: ^3.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
build_runner: ^2.7.1
riverpod_generator: ^3.0.0
riverpod_lint: ^3.0.0
json_serializable: ^6.11.1
flutter:
uses-material-design: true
پیکربندی گزینههای تحلیل
custom_lint به فایل analysis_options.yaml در ریشه پروژه خود اضافه کنید:
include: package:flutter_lints/flutter.yaml
analyzer:
plugins:
- custom_lint
این پیکربندی، lintهای مخصوص Riverpod را قادر میسازد تا به حفظ کیفیت کد کمک کنند.
پیادهسازی فایل main.dart
محتوای lib/main.dart را با موارد زیر جایگزین کنید:
lib/main.dart
import 'package:colorist_ui/colorist_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() async {
runApp(ProviderScope(child: MainApp()));
}
class MainApp extends ConsumerWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: MainScreen(
sendMessage: (message) {
sendMessage(message, ref);
},
),
);
}
// A fake LLM that just echoes back what it receives.
void sendMessage(String message, WidgetRef ref) {
final chatStateNotifier = ref.read(chatStateProvider.notifier);
final logStateNotifier = ref.read(logStateProvider.notifier);
chatStateNotifier.addUserMessage(message);
logStateNotifier.logUserText(message);
chatStateNotifier.addLlmMessage(message, MessageState.complete);
logStateNotifier.logLlmText(message);
}
}
این باعث میشود که یک برنامه Flutter یک سرویس echo پیادهسازی کند که با برگرداندن پیام کاربر، رفتار یک LLM را تقلید میکند.
درک معماری
بیایید کمی وقت بگذاریم تا معماری اپلیکیشن colorist را درک کنیم:
بستهی colorist_ui
بسته colorist_ui کامپوننتهای رابط کاربری از پیش ساخته شده و ابزارهای مدیریت حالت را ارائه میدهد:
- صفحه اصلی : کامپوننت اصلی رابط کاربری که موارد زیر را نمایش میدهد:
- طرحبندی تقسیم صفحه نمایش روی دسکتاپ (ناحیه تعامل و پنل گزارش)
- رابط کاربری تببندی شده در موبایل
- نمایشگر رنگی، رابط چت و تصاویر کوچک تاریخچه
- مدیریت وضعیت : این برنامه از چندین اعلان وضعیت استفاده میکند:
- ChatStateNotifier : پیامهای چت را مدیریت میکند.
- ColorStateNotifier : رنگ فعلی و تاریخچه آن را مدیریت میکند.
- LogStateNotifier : ورودیهای لاگ را برای اشکالزدایی مدیریت میکند.
- مدیریت پیام : این برنامه از یک مدل پیام با حالتهای مختلف استفاده میکند:
- پیامهای کاربر : توسط کاربر وارد شده است
- پیامهای LLM : تولید شده توسط LLM (یا فعلاً سرویس اکو شما)
- MessageState : پیگیری میکند که آیا پیامهای LLM کامل شدهاند یا هنوز در حال ارسال هستند.
معماری برنامه
این برنامه از معماری زیر پیروی میکند:
- لایه رابط کاربری : ارائه شده توسط بسته
colorist_ui - مدیریت وضعیت : از Riverpod برای مدیریت وضعیت واکنشی استفاده میکند.
- لایه سرویس : در حال حاضر شامل سرویس ساده echo شما است، این سرویس با سرویس چت Gemini جایگزین خواهد شد.
- ادغام LLM : در مراحل بعدی اضافه خواهد شد
این جداسازی به شما امکان میدهد تا بر پیادهسازی یکپارچهسازی LLM تمرکز کنید، در حالی که اجزای رابط کاربری از قبل مدیریت شدهاند.
برنامه را اجرا کنید
برنامه را با دستور زیر اجرا کنید:
flutter run -d DEVICE
به جای DEVICE ، نام دستگاه مورد نظر خود، مانند macos ، windows ، chrome یا شناسه دستگاه را وارد کنید.

اکنون باید برنامه Colorist را با موارد زیر مشاهده کنید:
- یک ناحیه نمایش رنگ با رنگ پیشفرض
- رابط چت که در آن میتوانید پیام تایپ کنید
- یک پنل گزارش که تعاملات چت را نشان میدهد
سعی کنید پیامی مانند «من یک رنگ آبی پررنگ میخواهم» را تایپ کنید و دکمه ارسال را فشار دهید. سرویس اکو به سادگی پیام شما را تکرار میکند. در مراحل بعدی، این را با تفسیر رنگ واقعی با استفاده از Firebase AI Logic جایگزین خواهید کرد.
بعدش چی؟
در مرحله بعد، Firebase را پیکربندی کرده و یکپارچهسازی اولیه Gemini API را برای جایگزینی سرویس echo خود با سرویس چت Gemini پیادهسازی خواهید کرد. این کار به برنامه اجازه میدهد تا توضیحات رنگ را تفسیر کرده و پاسخهای هوشمند ارائه دهد.
عیبیابی
مشکلات بسته رابط کاربری
اگر با بسته colorist_ui به مشکل برخوردید:
- مطمئن شوید که از آخرین نسخه استفاده میکنید
- تأیید کنید که وابستگی را به درستی اضافه کردهاید
- نسخههای متناقض بسته را بررسی کنید
خطاهای ساخت
اگر خطاهای ساخت را مشاهده کردید:
- مطمئن شوید که آخرین نسخه پایدار کانال Flutter SDK را نصب کردهاید.
- دستور
flutter cleanو به دنبال آنflutter pub getرا اجرا کنید. - خروجی کنسول را برای پیامهای خطای خاص بررسی کنید
مفاهیم کلیدی آموخته شده
- راهاندازی یک پروژه Flutter با وابستگیهای لازم
- درک معماری برنامه و مسئولیتهای اجزا
- پیادهسازی یک سرویس ساده که رفتار یک LLM را تقلید میکند
- اتصال سرویس به کامپوننتهای رابط کاربری
- استفاده از Riverpod برای مدیریت وضعیت
۳. یکپارچهسازی اولیه چت جمینی
در این مرحله، سرویس echo از مرحله قبل را با ادغام API Gemini با استفاده از Firebase AI Logic جایگزین خواهید کرد. Firebase را پیکربندی خواهید کرد، ارائه دهندگان لازم را تنظیم خواهید کرد و یک سرویس چت اولیه که با API Gemini ارتباط برقرار میکند، پیادهسازی خواهید کرد.
آنچه در این مرحله خواهید آموخت
- راهاندازی Firebase در یک برنامه Flutter
- پیکربندی منطق هوش مصنوعی فایربیس برای دسترسی به جمینی
- ایجاد ارائه دهندگان Riverpod برای سرویسهای Firebase و Gemini
- پیادهسازی یک سرویس چت پایه با Gemini API
- مدیریت پاسخهای API ناهمزمان و حالتهای خطا
فایربیس را راهاندازی کنید
ابتدا، باید Firebase را برای پروژه Flutter خود راهاندازی کنید. این شامل ایجاد یک پروژه Firebase، افزودن برنامه خود به آن و پیکربندی تنظیمات لازم Firebase AI Logic است.
ایجاد یک پروژه فایربیس
- به کنسول فایربیس بروید و با حساب گوگل خود وارد شوید.
- روی ایجاد یک پروژه Firebase کلیک کنید یا یک پروژه موجود را انتخاب کنید.
- برای ایجاد پروژه خود، مراحل نصب را دنبال کنید.
منطق هوش مصنوعی فایربیس را در پروژه فایربیس خود تنظیم کنید
- در کنسول Firebase، به پروژه خود بروید.
- در نوار کناری سمت چپ، هوش مصنوعی (AI) را انتخاب کنید.
- در منوی کشویی AI، گزینه AI Logic را انتخاب کنید.
- در کارت Firebase AI Logic، گزینه Get Started را انتخاب کنید.
- برای فعال کردن Gemini Developer API برای پروژه خود، دستورالعملها را دنبال کنید.
نصب رابط خط فرمان FlutterFire
رابط خط فرمان FlutterFire، راهاندازی Firebase را در برنامههای Flutter ساده میکند:
dart pub global activate flutterfire_cli
اضافه کردن فایربیس به برنامه فلاتر
- بستههای Firebase core و Firebase AI Logic را به پروژه خود اضافه کنید:
flutter pub add firebase_core firebase_ai
- دستور پیکربندی FlutterFire را اجرا کنید:
flutterfire configure
این دستور:
- از شما میخواهد پروژه Firebase که تازه ایجاد کردهاید را انتخاب کنید
- اپلیکیشنهای فلاتر خود را در فایربیس ثبت کنید
- یک فایل
firebase_options.dartبا پیکربندی پروژه خود ایجاد کنید
این دستور به طور خودکار پلتفرمهای انتخابی شما (iOS، اندروید، macOS، ویندوز، وب) را شناسایی کرده و آنها را به طور مناسب پیکربندی میکند.
پیکربندی مخصوص پلتفرم
فایربیس به حداقل نسخههای بالاتر از نسخههای پیشفرض فلاتر نیاز دارد. همچنین برای ارتباط با سرورهای Firebase AI Logic به دسترسی به شبکه نیاز دارد.
پیکربندی مجوزهای macOS
برای macOS، باید دسترسی به شبکه را در مجوزهای برنامه خود فعال کنید:
-
macos/Runner/DebugProfile.entitlementsرا باز کنید و موارد زیر را اضافه کنید:
macos/Runner/DebugProfile.entitlements
<key>com.apple.security.network.client</key>
<true/>
- همچنین
macos/Runner/Release.entitlementsرا باز کنید و همین ورودی را اضافه کنید.
تنظیمات iOS را پیکربندی کنید
برای iOS، حداقل نسخه را در بالای ios/Podfile بهروزرسانی کنید:
ios/پادفایل
# Firebase requires at least iOS 15.0
platform :ios, '15.0'
ایجاد ارائه دهندگان مدل Gemini
حالا شما ارائه دهندگان Riverpod را برای Firebase و Gemini ایجاد خواهید کرد. یک فایل جدید lib/providers/gemini.dart ایجاد کنید:
lib/providers/gemini.dart
import 'dart:async';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../firebase_options.dart';
part 'gemini.g.dart';
@Riverpod(keepAlive: true)
Future<FirebaseApp> firebaseApp(Ref ref) =>
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@Riverpod(keepAlive: true)
Future<GenerativeModel> geminiModel(Ref ref) async {
await ref.watch(firebaseAppProvider.future);
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-2.0-flash',
);
return model;
}
@Riverpod(keepAlive: true)
Future<ChatSession> chatSession(Ref ref) async {
final model = await ref.watch(geminiModelProvider.future);
return model.startChat();
}
این فایل مبنای سه ارائهدهندهی کلیدی را تعریف میکند. این ارائهدهندگان هنگام اجرای dart run build_runner توسط مولدهای کد Riverpod تولید میشوند. این کد از رویکرد مبتنی بر حاشیهنویسی Riverpod 3 با الگوهای ارائهدهندهی بهروز شده استفاده میکند.
-
firebaseAppProvider: فایربیس را با پیکربندی پروژه شما مقداردهی اولیه میکند. -
geminiModelProvider: یک نمونه مدل مولد Gemini ایجاد میکند. -
chatSessionProvider: یک جلسه چت با مدل Gemini ایجاد و نگهداری میکند.
حاشیهنویسی keepAlive: true در جلسه چت، تضمین میکند که در طول چرخه عمر برنامه باقی بماند و زمینه مکالمه را حفظ کند.
پیادهسازی سرویس چت Gemini
برای پیادهسازی سرویس چت، یک فایل جدید lib/services/gemini_chat_service.dart ایجاد کنید:
lib/services/gemini_chat_service.dart
import 'dart:async';
import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../providers/gemini.dart';
part 'gemini_chat_service.g.dart';
class GeminiChatService {
GeminiChatService(this.ref);
final Ref ref;
Future<void> sendMessage(String message) async {
final chatSession = await ref.read(chatSessionProvider.future);
final chatStateNotifier = ref.read(chatStateProvider.notifier);
final logStateNotifier = ref.read(logStateProvider.notifier);
chatStateNotifier.addUserMessage(message);
logStateNotifier.logUserText(message);
final llmMessage = chatStateNotifier.createLlmMessage();
try {
final response = await chatSession.sendMessage(Content.text(message));
final responseText = response.text;
if (responseText != null) {
logStateNotifier.logLlmText(responseText);
chatStateNotifier.appendToMessage(llmMessage.id, responseText);
}
} catch (e, st) {
logStateNotifier.logError(e, st: st);
chatStateNotifier.appendToMessage(
llmMessage.id,
"\nI'm sorry, I encountered an error processing your request. "
"Please try again.",
);
} finally {
chatStateNotifier.finalizeMessage(llmMessage.id);
}
}
}
@Riverpod(keepAlive: true)
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);
این سرویس:
- پیامهای کاربر را میپذیرد و آنها را به API Gemini ارسال میکند.
- رابط چت را با پاسخهای مدل بهروزرسانی میکند.
- برای سهولت درک جریان واقعی LLM، تمام ارتباطات را ثبت میکند.
- خطاها را با بازخورد مناسب کاربر مدیریت میکند
نکته: پنجرهی گزارش در این مرحله تقریباً مشابه پنجرهی چت خواهد بود. با اضافه کردن فراخوانیهای تابع و سپس پخش پاسخها، گزارش جالبتر خواهد شد.
تولید کد Riverpod
دستور build runner را اجرا کنید تا کد Riverpod لازم تولید شود:
dart run build_runner build --delete-conflicting-outputs
این دستور فایلهای .g.dart را که Riverpod برای عملکرد خود به آنها نیاز دارد، ایجاد میکند.
فایل main.dart را بهروزرسانی کنید
فایل lib/main.dart خود را برای استفاده از سرویس چت جدید Gemini بهروزرسانی کنید:
lib/main.dart
import 'package:colorist_ui/colorist_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'providers/gemini.dart';
import 'services/gemini_chat_service.dart';
void main() async {
runApp(ProviderScope(child: MainApp()));
}
class MainApp extends ConsumerWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final model = ref.watch(geminiModelProvider);
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: model.when(
data: (data) => MainScreen(
sendMessage: (text) {
ref.read(geminiChatServiceProvider).sendMessage(text);
},
),
loading: () => LoadingScreen(message: 'Initializing Gemini Model'),
error: (err, st) => ErrorScreen(error: err),
),
);
}
}
تغییرات کلیدی این بهروزرسانی عبارتند از:
- جایگزینی سرویس اکو با سرویس چت مبتنی بر Gemini API
- اضافه کردن صفحات بارگذاری و خطا با استفاده از الگوی
AsyncValueدر Riverpod با متدwhen - اتصال رابط کاربری به سرویس چت جدید از طریق فراخوانی
sendMessage
برنامه را اجرا کنید
برنامه را با دستور زیر اجرا کنید:
flutter run -d DEVICE
به جای DEVICE ، نام دستگاه مورد نظر خود، مانند macos ، windows ، chrome یا شناسه دستگاه را وارد کنید.

حالا وقتی پیامی تایپ میکنید، به API مربوط به Gemini ارسال میشود و به جای یک پیام انعکاسی، پاسخی از LLM دریافت خواهید کرد. پنل لاگ، تعاملات با API را نشان میدهد.
درک ارتباطات LLM
بیایید لحظهای وقت بگذاریم تا بفهمیم هنگام برقراری ارتباط با API Gemini چه اتفاقی میافتد:
جریان ارتباطی
- ورودی کاربر : کاربر متنی را در رابط چت وارد میکند.
- درخواست قالببندی : برنامه متن را به عنوان یک شیء
Contentبرای API Gemini قالببندی میکند. - ارتباط با API : متن از طریق منطق هوش مصنوعی فایربیس به API جمینی ارسال میشود.
- پردازش LLM : مدل Gemini متن را پردازش کرده و پاسخی تولید میکند.
- مدیریت پاسخ : برنامه پاسخ را دریافت کرده و رابط کاربری را بهروزرسانی میکند.
- ثبت وقایع : تمام ارتباطات برای شفافیت ثبت میشوند.
جلسات چت و زمینه گفتگو
جلسه چت Gemini زمینه بین پیامها را حفظ میکند و امکان تعاملات مکالمهای را فراهم میکند. این بدان معناست که LLM تبادلات قبلی در جلسه فعلی را "به خاطر میسپارد" و مکالمات منسجمتری را امکانپذیر میسازد.
حاشیهنویسی keepAlive: true در ارائهدهندهی جلسهی چت شما تضمین میکند که این زمینه در طول چرخهی حیات برنامه باقی بماند. این زمینهی پایدار برای حفظ جریان طبیعی مکالمه با LLM بسیار مهم است.
بعدش چی؟
در این مرحله، میتوانید از Gemini API هر چیزی بخواهید، زیرا هیچ محدودیتی در مورد آنچه که به آن پاسخ خواهد داد وجود ندارد. به عنوان مثال، میتوانید از آن خلاصهای از جنگهای گل رز بخواهید، که به هدف برنامه رنگی شما مربوط نیست.
در مرحله بعد، یک اعلان سیستمی ایجاد خواهید کرد تا Gemini را در تفسیر مؤثرتر توصیفات رنگ راهنمایی کند. این نشان میدهد که چگونه میتوانید رفتار یک LLM را برای نیازهای خاص برنامه سفارشی کنید و قابلیتهای آن را بر دامنه برنامه خود متمرکز کنید.
عیبیابی
مشکلات پیکربندی فایربیس
اگر در مقداردهی اولیه Firebase با خطا مواجه شدید:
- مطمئن شوید که فایل
firebase_options.dartشما به درستی تولید شده است. - تأیید کنید که برای دسترسی به Firebase AI Logic به طرح Blaze ارتقا یافتهاید.
خطاهای دسترسی به API
اگر در دسترسی به API Gemini خطایی دریافت کردید:
- تأیید کنید که صورتحساب به درستی در پروژه Firebase شما تنظیم شده است
- بررسی کنید که Firebase AI Logic و Cloud AI API در پروژه Firebase شما فعال باشند.
- تنظیمات اتصال شبکه و فایروال خود را بررسی کنید
- تأیید کنید که نام مدل (
gemini-2.0-flash) صحیح و در دسترس است
مسائل مربوط به زمینه گفتگو
اگر متوجه شدید که جمینی متن قبلی چت را به خاطر نمیآورد:
- تأیید کنید که تابع
chatSessionبا@Riverpod(keepAlive: true) حاشیهنویسی شده است. - بررسی کنید که آیا برای همه تبادل پیامها از یک جلسه چت استفاده میکنید یا خیر.
- قبل از ارسال پیام، تأیید کنید که جلسه چت به درستی راهاندازی شده است
مسائل مربوط به پلتفرم خاص
برای مشکلات خاص پلتفرم:
- iOS/macOS: اطمینان حاصل کنید که مجوزهای مناسب تنظیم شده و حداقل نسخهها پیکربندی شدهاند.
- اندروید: بررسی کنید که حداقل نسخه SDK به درستی تنظیم شده باشد
- پیامهای خطای مخصوص پلتفرم را در کنسول بررسی کنید
مفاهیم کلیدی آموخته شده
- راهاندازی Firebase در یک برنامه Flutter
- پیکربندی منطق هوش مصنوعی فایربیس برای دسترسی به جمینی
- ایجاد ارائه دهندگان Riverpod برای خدمات ناهمزمان
- پیادهسازی یک سرویس چت که با یک LLM ارتباط برقرار میکند
- مدیریت حالتهای ناهمزمان API (بارگذاری، خطا، دادهها)
- درک جریان ارتباطات LLM و جلسات چت
۴. راهنمایی مؤثر برای توصیف رنگها
در این مرحله، شما یک اعلان سیستمی ایجاد و پیادهسازی خواهید کرد که Gemini را در تفسیر توضیحات رنگ راهنمایی میکند. اعلانهای سیستمی روشی قدرتمند برای سفارشیسازی رفتار LLM برای وظایف خاص بدون تغییر کد شما هستند.
آنچه در این مرحله خواهید آموخت
- درک دستورات سیستم و اهمیت آنها در برنامههای LLM
- ایجاد دستورالعملهای مؤثر برای وظایف خاص دامنه
- بارگیری و استفاده از اعلانهای سیستم در یک برنامه Flutter
- راهنمایی یک LLM برای ارائه پاسخهای با فرمت ثابت
- آزمایش چگونگی تأثیر اعلانهای سیستم بر رفتار LLM
آشنایی با دستورات سیستم
قبل از پرداختن به پیادهسازی، بیایید بفهمیم که اعلانهای سیستمی چیستند و چرا مهم هستند:
پیامهای سیستمی چیستند؟
یک اعلان سیستمی نوع خاصی از دستورالعمل است که به یک LLM داده میشود و زمینه، دستورالعملهای رفتاری و انتظارات را برای پاسخهای آن تعیین میکند. برخلاف پیامهای کاربر، اعلانهای سیستمی:
- نقش و شخصیت LLM را مشخص کنید
- تعریف دانش یا قابلیتهای تخصصی
- ارائه دستورالعملهای قالببندی
- محدودیتهایی برای پاسخها تعیین کنید
- نحوه مدیریت سناریوهای مختلف را شرح دهید
به یک پیام سیستمی به عنوان ارائه «شرح وظایف» به LLM فکر کنید - این پیام به مدل میگوید که در طول مکالمه چگونه رفتار کند.
چرا اعلانهای سیستم مهم هستند؟
اعلانهای سیستم برای ایجاد تعاملات LLM سازگار و مفید بسیار مهم هستند زیرا:
- تضمین ثبات : مدل را هدایت کنید تا پاسخها را در قالبی ثابت ارائه دهد.
- بهبود ارتباط : مدل را روی دامنه خاص خود (در مورد شما، رنگها) متمرکز کنید.
- تعیین مرزها : تعریف کنید که مدل چه کاری باید انجام دهد و چه کاری نباید انجام دهد
- بهبود تجربه کاربری : ایجاد یک الگوی تعاملی طبیعیتر و مفیدتر
- کاهش پسپردازش : دریافت پاسخها در قالبهایی که تجزیه یا نمایش آنها آسانتر است
برای برنامه Colorist خود، به LLM نیاز دارید تا به طور مداوم توضیحات رنگ را تفسیر کند و مقادیر RGB را در قالب خاصی ارائه دهد.
یک فایل اعلان سیستم ایجاد کنید
ابتدا، یک فایل اعلان سیستم ایجاد خواهید کرد که در زمان اجرا بارگذاری میشود. این رویکرد به شما امکان میدهد اعلان را بدون کامپایل مجدد برنامه خود تغییر دهید.
یک فایل جدید assets/system_prompt.md با محتوای زیر ایجاد کنید:
assets/system_prompt.md
# Colorist System Prompt
You are a color expert assistant integrated into a desktop app called Colorist. Your job is to interpret natural language color descriptions and provide the appropriate RGB values that best represent that description.
## Your Capabilities
You are knowledgeable about colors, color theory, and how to translate natural language descriptions into specific RGB values. When users describe a color, you should:
1. Analyze their description to understand the color they are trying to convey
2. Determine the appropriate RGB values (values should be between 0.0 and 1.0)
3. Respond with a conversational explanation and explicitly state the RGB values
## How to Respond to User Inputs
When users describe a color:
1. First, acknowledge their color description with a brief, friendly response
2. Interpret what RGB values would best represent that color description
3. Always include the RGB values clearly in your response, formatted as: `RGB: (red=X.X, green=X.X, blue=X.X)`
4. Provide a brief explanation of your interpretation
Example:
User: "I want a sunset orange"
You: "Sunset orange is a warm, vibrant color that captures the golden-red hues of the setting sun. It combines a strong red component with moderate orange tones.
RGB: (red=1.0, green=0.5, blue=0.25)
I've selected values with high red, moderate green, and low blue to capture that beautiful sunset glow. This creates a warm orange with a slightly reddish tint, reminiscent of the sun low on the horizon."
## When Descriptions are Unclear
If a color description is ambiguous or unclear, please ask the user clarifying questions, one at a time.
## Important Guidelines
- Always keep RGB values between 0.0 and 1.0
- Always format RGB values as: `RGB: (red=X.X, green=X.X, blue=X.X)` for easy parsing
- Provide thoughtful, knowledgeable responses about colors
- When possible, include color psychology, associations, or interesting facts about colors
- Be conversational and engaging in your responses
- Focus on being helpful and accurate with your color interpretations
آشنایی با ساختار اعلان سیستم
بیایید بررسی کنیم که این اعلان چه کاری انجام میدهد:
- تعریف نقش : مدرک کارشناسی ارشد مدیریت بازرگانی (LLM) را به عنوان "دستیار متخصص رنگ" تأسیس میکند.
- شرح وظیفه : وظیفه اصلی را تفسیر توصیفات رنگ به مقادیر RGB تعریف میکند.
- قالب پاسخ : دقیقاً مشخص میکند که مقادیر RGB برای حفظ سازگاری چگونه قالببندی شوند.
- تبادل مثال : یک مثال ملموس از الگوی تعامل مورد انتظار ارائه میدهد.
- مدیریت حروف بزرگ و کوچک در لبهها : نحوه مدیریت توضیحات نامشخص را آموزش میدهد.
- محدودیتها و دستورالعملها : مرزهایی مانند نگه داشتن مقادیر RGB بین ۰.۰ و ۱.۰ را تعیین میکند.
این رویکرد ساختاریافته تضمین میکند که پاسخهای LLM سازگار، آموزنده و به گونهای قالببندی شوند که اگر بخواهید مقادیر RGB را به صورت برنامهنویسی استخراج کنید، تجزیه آنها آسان باشد.
بهروزرسانی pubspec.yaml
اکنون، پایین فایل pubspec.yaml خود را بهروزرسانی کنید تا دایرکتوری assets را شامل شود:
pubspec.yaml
flutter:
uses-material-design: true
assets:
- assets/
برای بهروزرسانی بستهی داراییها، flutter pub get را اجرا کنید.
ایجاد یک ارائه دهنده اعلان سیستم
یک فایل جدید lib/providers/system_prompt.dart ایجاد کنید تا اعلان سیستم (system prompt) را بارگذاری کند:
lib/providers/system_prompt.dart
import 'package:flutter/services.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'system_prompt.g.dart';
@Riverpod(keepAlive: true)
Future<String> systemPrompt(Ref ref) =>
rootBundle.loadString('assets/system_prompt.md');
این ارائهدهنده از سیستم بارگذاری داراییهای فلاتر برای خواندن فایل اعلان در زمان اجرا استفاده میکند.
ارائه دهنده مدل Gemini را به روز کنید
اکنون فایل lib/providers/gemini.dart خود را طوری تغییر دهید که اعلان سیستم را نیز شامل شود:
lib/providers/gemini.dart
import 'dart:async';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../firebase_options.dart';
import 'system_prompt.dart'; // Add this import
part 'gemini.g.dart';
@Riverpod(keepAlive: true)
Future<FirebaseApp> firebaseApp(Ref ref) =>
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@Riverpod(keepAlive: true)
Future<GenerativeModel> geminiModel(Ref ref) async {
await ref.watch(firebaseAppProvider.future);
final systemPrompt = await ref.watch(systemPromptProvider.future); // Add this line
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-2.0-flash',
systemInstruction: Content.system(systemPrompt), // And this line
);
return model;
}
@Riverpod(keepAlive: true)
Future<ChatSession> chatSession(Ref ref) async {
final model = await ref.watch(geminiModelProvider.future);
return model.startChat();
}
تغییر کلیدی، اضافه کردن systemInstruction: Content.system(systemPrompt) هنگام ایجاد مدل مولد است. این به Gemini میگوید که از دستورالعملهای شما به عنوان اعلان سیستم برای همه تعاملات در این جلسه چت استفاده کند.
تولید کد Riverpod
دستور build runner را اجرا کنید تا کد Riverpod مورد نیاز تولید شود:
dart run build_runner build --delete-conflicting-outputs
اجرا و تست اپلیکیشن
حالا برنامهی خود را اجرا کنید:
flutter run -d DEVICE

سعی کنید آن را با توصیفهای رنگی مختلف آزمایش کنید:
- «من آبی آسمانی میخواهم»
- «به من یک رنگ سبز جنگلی بده»
- «یک رنگ نارنجی غروب آفتاب پر جنب و جوش درست کنید»
- «من رنگ اسطوخودوس تازه را میخواهم»
- «چیزی شبیه آبیِ اعماق اقیانوس به من نشان بده»
باید توجه داشته باشید که Gemini اکنون با توضیحات محاورهای در مورد رنگها به همراه مقادیر RGB با فرمت ثابت پاسخ میدهد. اعلان سیستم به طور مؤثر LLM را راهنمایی کرده است تا نوع پاسخهای مورد نیاز شما را ارائه دهد.
همچنین سعی کنید از آن بخواهید محتوایی خارج از زمینه رنگها ارائه دهد. مثلاً دلایل اصلی جنگ رزها. باید متوجه تفاوتی نسبت به مرحله قبل شوید.
اهمیت مهندسی سریع برای کارهای تخصصی
اعلانهای سیستم هم هنر و هم علم هستند. آنها بخش مهمی از ادغام LLM هستند که میتوانند به طور چشمگیری بر میزان مفید بودن مدل برای کاربرد خاص شما تأثیر بگذارند. کاری که شما در اینجا انجام دادهاید نوعی مهندسی اعلان است - تنظیم دستورالعملها برای اینکه مدل به گونهای رفتار کند که متناسب با نیازهای کاربرد شما باشد.
مهندسی سریع و مؤثر شامل موارد زیر است:
- تعریف واضح نقش : تعیین هدف LLM
- دستورالعملهای صریح : جزئیات دقیق نحوه پاسخگویی LLM
- مثالهای ملموس : نشان دادن به جای صرفاً گفتن اینکه پاسخهای خوب چگونه هستند
- مدیریت موارد حاشیهای : آموزش نحوه برخورد با سناریوهای مبهم به LLM
- مشخصات قالببندی : اطمینان از ساختارمند بودن و قابل استفاده بودن پاسخها
اعلان سیستمی که ایجاد کردهاید، قابلیتهای عمومی Gemini را به یک دستیار تفسیر رنگ تخصصی تبدیل میکند که پاسخهایی را ارائه میدهد که بهطور خاص برای نیازهای برنامه شما قالببندی شدهاند. این یک الگوی قدرتمند است که میتوانید در بسیاری از حوزهها و وظایف مختلف اعمال کنید.
بعدش چی؟
در مرحله بعد، با اضافه کردن تعریف توابع، بر این پایه بنا خواهید کرد، که به LLM اجازه میدهد نه تنها مقادیر RGB را پیشنهاد دهد، بلکه در واقع توابع موجود در برنامه شما را برای تنظیم مستقیم رنگ فراخوانی کند. این نشان میدهد که چگونه LLMها میتوانند شکاف بین زبان طبیعی و ویژگیهای کاربردی ملموس را پر کنند.
عیبیابی
مشکلات بارگذاری دارایی
اگر در بارگذاری اعلان سیستم با خطایی مواجه شدید:
- تأیید کنید که
pubspec.yamlشما به درستی فهرست دایرکتوری assets را فهرست میکند. - بررسی کنید که مسیر موجود در
rootBundle.loadString()با محل فایل شما مطابقت داشته باشد. - دستور
flutter cleanو به دنبال آنflutter pub getرا اجرا کنید تا بستهی داراییها بهروزرسانی شود.
پاسخهای متناقض
اگر LLM به طور مداوم دستورالعملهای قالببندی شما را دنبال نمیکند:
- سعی کنید الزامات قالببندی را در اعلان سیستم صریحتر کنید
- مثالهای بیشتری برای نشان دادن الگوی مورد انتظار اضافه کنید
- مطمئن شوید که فرمتی که درخواست میکنید برای مدل مورد نظر معقول است.
محدود کردن نرخ API
اگر با خطاهای مربوط به محدود کردن نرخ مواجه شدید:
- توجه داشته باشید که سرویس Firebase AI Logic محدودیتهای استفاده دارد.
- پیادهسازی منطق تلاش مجدد با backoff نمایی را در نظر بگیرید.
- کنسول فایربیس خود را برای هرگونه مشکل سهمیهبندی بررسی کنید
مفاهیم کلیدی آموخته شده
- درک نقش و اهمیت اعلانهای سیستم در برنامههای LLM
- ایجاد دستورالعملهای مؤثر با دستورالعملها، مثالها و محدودیتهای واضح
- بارگیری و استفاده از اعلانهای سیستم در یک برنامه Flutter
- هدایت رفتار LLM برای وظایف خاص دامنه
- استفاده از مهندسی سریع برای شکلدهی به پاسخهای LLM
این مرحله نشان میدهد که چگونه میتوانید بدون تغییر کد خود - صرفاً با ارائه دستورالعملهای واضح در اعلان سیستم - به سفارشیسازی قابل توجهی در رفتار LLM دست یابید.
۵. تعریف توابع برای ابزارهای LLM
در این مرحله، شما کار فعال کردن Gemini برای انجام اقدامات در برنامه خود را با پیادهسازی اعلانهای تابع آغاز خواهید کرد. این ویژگی قدرتمند به LLM اجازه میدهد تا نه تنها مقادیر RGB را پیشنهاد دهد، بلکه آنها را از طریق فراخوانیهای ابزار تخصصی در رابط کاربری برنامه شما تنظیم کند. با این حال، در مرحله بعدی، مشاهده درخواستهای LLM اجرا شده در برنامه Flutter مورد نیاز خواهد بود.
آنچه در این مرحله خواهید آموخت
- درک فراخوانی توابع LLM و مزایای آن برای برنامههای Flutter
- تعریف اعلانهای تابع مبتنی بر طرحواره برای Gemini
- ادغام اعلانهای تابع با مدل Gemini شما
- بهروزرسانی اعلان سیستم برای استفاده از قابلیتهای ابزار
آشنایی با فراخوانی توابع
قبل از پیادهسازی تعریف توابع، بیایید بفهمیم که آنها چه هستند و چرا ارزشمندند:
فراخوانی تابع چیست؟
فراخوانی تابع (که گاهی اوقات "استفاده از ابزار" نامیده میشود) قابلیتی است که به یک LLM اجازه میدهد:
- تشخیص دهید چه زمانی یک درخواست کاربر از فراخوانی یک تابع خاص سود میبرد
- یک شیء JSON ساختاریافته با پارامترهای مورد نیاز برای آن تابع تولید کنید.
- بگذارید برنامه شما تابع را با آن پارامترها اجرا کند
- نتیجه تابع را دریافت کرده و آن را در پاسخ آن بگنجانید
به جای اینکه LLM فقط توصیف کند که چه کاری باید انجام شود، فراخوانی تابع، LLM را قادر میسازد تا اقدامات مشخصی را در برنامه شما انجام دهد.
چرا فراخوانی تابع برای برنامههای فلاتر اهمیت دارد؟
فراخوانی تابع، پلی قدرتمند بین زبان طبیعی و ویژگیهای برنامه ایجاد میکند:
- اقدام مستقیم : کاربران میتوانند آنچه را که میخواهند به زبان طبیعی توصیف کنند و برنامه با اقدامات مشخص پاسخ میدهد.
- خروجی ساختاریافته : LLM به جای متنی که نیاز به تجزیه دارد، دادههای تمیز و ساختاریافته تولید میکند.
- عملیات پیچیده : LLM را قادر میسازد تا به دادههای خارجی دسترسی پیدا کند، محاسبات را انجام دهد یا وضعیت برنامه را تغییر دهد.
- تجربه کاربری بهتر : یکپارچهسازی بینقصی بین مکالمه و عملکرد ایجاد میکند.
در برنامه Colorist شما، فراخوانی تابع به کاربران این امکان را میدهد که بگویند "من رنگ سبز جنگلی میخواهم" و رابط کاربری بلافاصله با آن رنگ بهروزرسانی شود، بدون اینکه نیازی به تجزیه مقادیر RGB از متن باشد.
تعریف اعلانهای تابع
یک فایل جدید lib/services/gemini_tools.dart ایجاد کنید تا اعلانهای تابع خود را در آن تعریف کنید:
lib/services/gemini_tools.dart
import 'package:firebase_ai/firebase_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'gemini_tools.g.dart';
class GeminiTools {
GeminiTools(this.ref);
final Ref ref;
FunctionDeclaration get setColorFuncDecl => FunctionDeclaration(
'set_color',
'Set the color of the display square based on red, green, and blue values.',
parameters: {
'red': Schema.number(description: 'Red component value (0.0 - 1.0)'),
'green': Schema.number(description: 'Green component value (0.0 - 1.0)'),
'blue': Schema.number(description: 'Blue component value (0.0 - 1.0)'),
},
);
List<Tool> get tools => [
Tool.functionDeclarations([setColorFuncDecl]),
];
}
@Riverpod(keepAlive: true)
GeminiTools geminiTools(Ref ref) => GeminiTools(ref);
درک تعریف توابع
بیایید ببینیم این کد چه کاری انجام میدهد:
- نامگذاری تابع : شما تابع خود را
set_colorنامگذاری میکنید تا هدف آن را به وضوح نشان دهد. - شرح تابع : شما توضیح واضحی ارائه میدهید که به LLM کمک میکند تا بفهمد چه زمانی از آن استفاده کند.
- تعاریف پارامتر : شما پارامترهای ساختاریافته را با توضیحات خاص خودشان تعریف میکنید:
-
red: جزء قرمز RGB، که به صورت عددی بین 0.0 و 1.0 مشخص میشود. -
green: مؤلفه سبز RGB، که به صورت عددی بین ۰.۰ و ۱.۰ مشخص میشود. -
blue: مؤلفه آبی RGB، که به صورت عددی بین 0.0 و 1.0 مشخص میشود.
-
- انواع طرحواره : شما از
Schema.number()برای نشان دادن مقادیر عددی استفاده میکنید. - مجموعه ابزارها : شما فهرستی از ابزارهایی که شامل تعریف تابع شما هستند را ایجاد میکنید.
این رویکرد ساختاریافته به دانشجویان کارشناسی ارشد مدیریت بازرگانی (LLM) در دانشگاه Gemini کمک میکند تا موارد زیر را درک کنند:
- چه زمانی باید این تابع را فراخوانی کند
- چه پارامترهایی را باید ارائه دهد
- چه محدودیتهایی برای آن پارامترها اعمال میشود (مانند محدوده مقادیر)
ارائه دهنده مدل Gemini را به روز کنید
اکنون، فایل lib/providers/gemini.dart خود را طوری تغییر دهید که هنگام مقداردهی اولیه مدل Gemini، اعلانهای تابع را نیز شامل شود:
lib/providers/gemini.dart
import 'dart:async';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../firebase_options.dart';
import '../services/gemini_tools.dart'; // Add this import
import 'system_prompt.dart';
part 'gemini.g.dart';
@Riverpod(keepAlive: true)
Future<FirebaseApp> firebaseApp(Ref ref) =>
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@Riverpod(keepAlive: true)
Future<GenerativeModel> geminiModel(Ref ref) async {
await ref.watch(firebaseAppProvider.future);
final systemPrompt = await ref.watch(systemPromptProvider.future);
final geminiTools = ref.watch(geminiToolsProvider); // Add this line
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-2.0-flash',
systemInstruction: Content.system(systemPrompt),
tools: geminiTools.tools, // And this line
);
return model;
}
@Riverpod(keepAlive: true)
Future<ChatSession> chatSession(Ref ref) async {
final model = await ref.watch(geminiModelProvider.future);
return model.startChat();
}
تغییر کلیدی، اضافه کردن پارامتر tools: geminiTools.tools هنگام ایجاد مدل مولد است. این کار باعث میشود Gemini از توابعی که برای فراخوانی در دسترس هستند، آگاه شود.
بهروزرسانی اعلان سیستم
حالا باید اعلان سیستم خود را تغییر دهید تا به LLM در مورد استفاده از ابزار جدید set_color دستور دهید. assets/system_prompt.md را بهروزرسانی کنید:
assets/system_prompt.md
# Colorist System Prompt
You are a color expert assistant integrated into a desktop app called Colorist. Your job is to interpret natural language color descriptions and set the appropriate color values using a specialized tool.
## Your Capabilities
You are knowledgeable about colors, color theory, and how to translate natural language descriptions into specific RGB values. You have access to the following tool:
`set_color` - Sets the RGB values for the color display based on a description
## How to Respond to User Inputs
When users describe a color:
1. First, acknowledge their color description with a brief, friendly response
2. Interpret what RGB values would best represent that color description
3. Use the `set_color` tool to set those values (all values should be between 0.0 and 1.0)
4. After setting the color, provide a brief explanation of your interpretation
Example:
User: "I want a sunset orange"
You: "Sunset orange is a warm, vibrant color that captures the golden-red hues of the setting sun. It combines a strong red component with moderate orange tones."
[Then you would call the set_color tool with approximately: red=1.0, green=0.5, blue=0.25]
After the tool call: "I've set a warm orange with strong red, moderate green, and minimal blue components that is reminiscent of the sun low on the horizon."
## When Descriptions are Unclear
If a color description is ambiguous or unclear, please ask the user clarifying questions, one at a time.
## Important Guidelines
- Always keep RGB values between 0.0 and 1.0
- Provide thoughtful, knowledgeable responses about colors
- When possible, include color psychology, associations, or interesting facts about colors
- Be conversational and engaging in your responses
- Focus on being helpful and accurate with your color interpretations
تغییرات کلیدی در اعلان سیستم عبارتند از:
- معرفی ابزار : به جای درخواست مقادیر RGB فرمتشده، اکنون ابزار
set_colorرا به LLM اطلاع میدهید. - فرآیند اصلاحشده : شما مرحله ۳ را از «قالببندی مقادیر در پاسخ» به «استفاده از ابزار برای تنظیم مقادیر» تغییر میدهید.
- مثال بهروزرسانیشده : شما نشان میدهید که چگونه پاسخ باید شامل یک فراخوانی ابزار به جای متن قالببندیشده باشد.
- الزام قالببندی حذف شد : از آنجایی که از فراخوانیهای تابع ساختاریافته استفاده میکنید، دیگر نیازی به قالببندی متن خاصی ندارید.
این اعلان بهروزرسانیشده، LLM را هدایت میکند تا به جای ارائه مقادیر RGB به صورت متنی، از فراخوانی تابع استفاده کند.
تولید کد Riverpod
دستور build runner را اجرا کنید تا کد Riverpod مورد نیاز تولید شود:
dart run build_runner build --delete-conflicting-outputs
برنامه را اجرا کنید
در این مرحله، Gemini محتوایی تولید میکند که سعی در استفاده از فراخوانی تابع دارد، اما شما هنوز هندلرهایی برای فراخوانیهای تابع پیادهسازی نکردهاید. وقتی برنامه را اجرا میکنید و یک رنگ را توصیف میکنید، خواهید دید که Gemini طوری پاسخ میدهد که انگار ابزاری را فراخوانی کرده است، اما تا مرحله بعدی هیچ تغییر رنگی در رابط کاربری مشاهده نخواهید کرد.
برنامه خود را اجرا کنید:
flutter run -d DEVICE

سعی کنید رنگی مانند «آبی اقیانوسی تیره» یا «سبز جنگلی» را توصیف کنید و پاسخها را مشاهده کنید. LLM در تلاش است تا توابع تعریف شده در بالا را فراخوانی کند، اما کد شما هنوز فراخوانیهای تابع را تشخیص نمیدهد.
فرآیند فراخوانی تابع
بیایید بفهمیم وقتی Gemini از فراخوانی تابع استفاده میکند چه اتفاقی میافتد:
- انتخاب تابع : LLM بر اساس درخواست کاربر تصمیم میگیرد که آیا فراخوانی یک تابع مفید خواهد بود یا خیر.
- تولید پارامتر : LLM مقادیر پارامتری را تولید میکند که با طرحواره تابع مطابقت دارند.
- قالب فراخوانی تابع : LLM یک شیء فراخوانی تابع ساختاریافته را در پاسخ خود ارسال میکند.
- مدیریت برنامه : برنامه شما این فراخوانی را دریافت کرده و تابع مربوطه را اجرا میکند (که در مرحله بعدی پیادهسازی شده است).
- ادغام پاسخ : در مکالمات چند نوبتی، LLM انتظار دارد که نتیجه تابع برگردانده شود
در وضعیت فعلی برنامه شما، سه مرحله اول در حال انجام هستند، اما شما هنوز مرحله ۴ یا ۵ (مدیریت فراخوانیهای تابع) را پیادهسازی نکردهاید، که در مرحله بعدی انجام خواهید داد.
جزئیات فنی: چگونه Gemini تصمیم میگیرد چه زمانی از توابع استفاده کند
Gemini تصمیمات هوشمندانهای در مورد زمان استفاده از توابع بر اساس موارد زیر میگیرد:
- قصد کاربر : اینکه آیا درخواست کاربر به بهترین شکل توسط یک تابع ارائه میشود یا خیر
- مرتبط بودن عملکرد : اینکه عملکردهای موجود چقدر با وظیفه مطابقت دارند
- در دسترس بودن پارامتر : آیا میتوان با اطمینان مقادیر پارامتر را تعیین کرد؟
- دستورالعملهای سیستم : راهنمایی از طریق اعلان سیستم شما در مورد نحوه استفاده از عملکرد
با ارائه تعریفهای واضح تابع و دستورالعملهای سیستمی، شما Gemini را طوری تنظیم کردهاید که درخواستهای توصیف رنگ را به عنوان فرصتهایی برای فراخوانی تابع set_color تشخیص دهد.
بعدش چی؟
در مرحله بعد، شما هندلرهایی را برای فراخوانیهای تابع از Gemini پیادهسازی خواهید کرد. این کار حلقه را کامل میکند و به توضیحات کاربر اجازه میدهد تا از طریق فراخوانیهای تابع LLM، تغییرات رنگ واقعی را در رابط کاربری ایجاد کنند.
عیبیابی
مشکلات مربوط به اعلان تابع
اگر در تعریف توابع با خطا مواجه شدید:
- بررسی کنید که نامها و انواع پارامترها با آنچه انتظار میرود مطابقت داشته باشند
- تأیید کنید که نام تابع واضح و توصیفی است
- اطمینان حاصل کنید که توضیحات تابع، هدف آن را به طور دقیق توضیح میدهد.
مشکلات اعلان سیستم
اگر LLM سعی در استفاده از تابع ندارد:
- تأیید کنید که اعلان سیستم شما به وضوح به LLM دستور میدهد که از ابزار
set_colorاستفاده کند. - بررسی کنید که مثال موجود در اعلان سیستم، نحوهی استفاده از تابع را نشان دهد
- سعی کنید دستورالعمل استفاده از ابزار را واضحتر کنید
مسائل عمومی
اگر با مشکلات دیگری مواجه شدید:
- کنسول را برای هرگونه خطای مربوط به تعریف توابع بررسی کنید.
- تأیید کنید که ابزارها به درستی به مدل منتقل شدهاند
- اطمینان حاصل کنید که تمام کدهای تولید شده توسط Riverpod بهروز هستند.
مفاهیم کلیدی آموخته شده
- تعریف اعلانهای تابع برای گسترش قابلیتهای LLM در برنامههای Flutter
- ایجاد طرحوارههای پارامتری برای جمعآوری دادههای ساختاریافته
- ادغام اعلانهای تابع با مدل Gemini
- بهروزرسانی پیامهای سیستم برای تشویق به استفاده از عملکرد
- درک نحوه انتخاب و فراخوانی توابع توسط LLMها
این مرحله نشان میدهد که چگونه LLMها میتوانند شکاف بین ورودی زبان طبیعی و فراخوانیهای تابع ساختاریافته را پر کنند و زمینه را برای ادغام یکپارچه بین ویژگیهای مکالمه و برنامه فراهم کنند.
۶. پیادهسازی جابجایی ابزار
در این مرحله، شما هندلرهایی را برای فراخوانیهای تابع از Gemini پیادهسازی خواهید کرد. این کار حلقه ارتباط بین ورودیهای زبان طبیعی و ویژگیهای کاربردی ملموس را تکمیل میکند و به LLM اجازه میدهد تا رابط کاربری شما را بر اساس توضیحات کاربر مستقیماً دستکاری کند.
آنچه در این مرحله خواهید آموخت
- درک کامل خط لوله فراخوانی تابع در برنامههای LLM
- Processing function calls from Gemini in a Flutter application
- Implementing function handlers that modify application state
- Handling function responses and returning results to the LLM
- Creating a complete communication flow between LLM and UI
- Logging function calls and responses for transparency
Understanding the function calling pipeline
Before diving into implementation, let's understand the complete function calling pipeline:
The end-to-end flow
- User input : User describes a color in natural language (eg, "forest green")
- LLM processing : Gemini analyzes the description and decides to call the
set_colorfunction - Function call generation : Gemini creates a structured JSON with parameters (red, green, blue values)
- Function call reception : Your app receives this structured data from Gemini
- Function execution : Your app executes the function with the provided parameters
- State update : The function updates your app's state (changing the displayed color)
- Response generation : Your function returns results back to the LLM
- Response incorporation : The LLM incorporates these results into its final response
- UI update : Your UI reacts to the state change, displaying the new color
The complete communication cycle is essential for proper LLM integration. When an LLM makes a function call, it doesn't simply send the request and move on. Instead, it waits for your application to execute the function and return results. The LLM then uses these results to formulate its final response, creating a natural conversation flow that acknowledges the actions taken.
Implement function handlers
Let's update your lib/services/gemini_tools.dart file to add handlers for function calls:
lib/services/gemini_tools.dart
import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'gemini_tools.g.dart';
class GeminiTools {
GeminiTools(this.ref);
final Ref ref;
FunctionDeclaration get setColorFuncDecl => FunctionDeclaration(
'set_color',
'Set the color of the display square based on red, green, and blue values.',
parameters: {
'red': Schema.number(description: 'Red component value (0.0 - 1.0)'),
'green': Schema.number(description: 'Green component value (0.0 - 1.0)'),
'blue': Schema.number(description: 'Blue component value (0.0 - 1.0)'),
},
);
List<Tool> get tools => [
Tool.functionDeclarations([setColorFuncDecl]),
];
Map<String, Object?> handleFunctionCall( // Add from here
String functionName,
Map<String, Object?> arguments,
) {
final logStateNotifier = ref.read(logStateProvider.notifier);
logStateNotifier.logFunctionCall(functionName, arguments);
return switch (functionName) {
'set_color' => handleSetColor(arguments),
_ => handleUnknownFunction(functionName),
};
}
Map<String, Object?> handleSetColor(Map<String, Object?> arguments) {
final colorStateNotifier = ref.read(colorStateProvider.notifier);
final red = (arguments['red'] as num).toDouble();
final green = (arguments['green'] as num).toDouble();
final blue = (arguments['blue'] as num).toDouble();
final functionResults = {
'success': true,
'current_color': colorStateNotifier
.updateColor(red: red, green: green, blue: blue)
.toLLMContextMap(),
};
final logStateNotifier = ref.read(logStateProvider.notifier);
logStateNotifier.logFunctionResults(functionResults);
return functionResults;
}
Map<String, Object?> handleUnknownFunction(String functionName) {
final logStateNotifier = ref.read(logStateProvider.notifier);
logStateNotifier.logWarning('Unsupported function call $functionName');
return {
'success': false,
'reason': 'Unsupported function call $functionName',
};
} // To here.
}
@Riverpod(keepAlive: true)
GeminiTools geminiTools(Ref ref) => GeminiTools(ref);
Understanding the function handlers
Let's break down what these function handlers do:
-
handleFunctionCall: A central dispatcher that:- Logs the function call for transparency in the log panel
- Routes to the appropriate handler based on the function name
- Returns a structured response that will be sent back to the LLM
-
handleSetColor: The specific handler for yourset_colorfunction that:- Extracts RGB values from the arguments map
- Converts them to the expected types (doubles)
- Updates the application's color state using the
colorStateNotifier - Creates a structured response with success status and current color information
- Logs the function results for debugging
-
handleUnknownFunction: A fallback handler for unknown functions that:- Logs a warning about the unsupported function
- Returns an error response to the LLM
The handleSetColor function is particularly important as it bridges the gap between the LLM's natural language understanding and concrete UI changes.
Update the Gemini chat service to process function calls and responses
Now, let's update the lib/services/gemini_chat_service.dart file to process function calls from the LLM responses and send the results back to the LLM:
lib/services/gemini_chat_service.dart
import 'dart:async';
import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../providers/gemini.dart';
import 'gemini_tools.dart'; // Add this import
part 'gemini_chat_service.g.dart';
class GeminiChatService {
GeminiChatService(this.ref);
final Ref ref;
Future<void> sendMessage(String message) async {
final chatSession = await ref.read(chatSessionProvider.future);
final chatStateNotifier = ref.read(chatStateProvider.notifier);
final logStateNotifier = ref.read(logStateProvider.notifier);
chatStateNotifier.addUserMessage(message);
logStateNotifier.logUserText(message);
final llmMessage = chatStateNotifier.createLlmMessage();
try {
final response = await chatSession.sendMessage(Content.text(message));
final responseText = response.text;
if (responseText != null) {
logStateNotifier.logLlmText(responseText);
chatStateNotifier.appendToMessage(llmMessage.id, responseText);
}
if (response.functionCalls.isNotEmpty) { // Add from here
final geminiTools = ref.read(geminiToolsProvider);
final functionResultResponse = await chatSession.sendMessage(
Content.functionResponses([
for (final functionCall in response.functionCalls)
FunctionResponse(
functionCall.name,
geminiTools.handleFunctionCall(
functionCall.name,
functionCall.args,
),
),
]),
);
final responseText = functionResultResponse.text;
if (responseText != null) {
logStateNotifier.logLlmText(responseText);
chatStateNotifier.appendToMessage(llmMessage.id, responseText);
}
} // To here.
} catch (e, st) {
logStateNotifier.logError(e, st: st);
chatStateNotifier.appendToMessage(
llmMessage.id,
"\nI'm sorry, I encountered an error processing your request. "
"Please try again.",
);
} finally {
chatStateNotifier.finalizeMessage(llmMessage.id);
}
}
}
@Riverpod(keepAlive: true)
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);
Understanding the flow of communication
The key addition here is the complete handling of function calls and responses:
if (response.functionCalls.isNotEmpty) {
final geminiTools = ref.read(geminiToolsProvider);
final functionResultResponse = await chatSession.sendMessage(
Content.functionResponses([
for (final functionCall in response.functionCalls)
FunctionResponse(
functionCall.name,
geminiTools.handleFunctionCall(
functionCall.name,
functionCall.args,
),
),
]),
);
final responseText = functionResultResponse.text;
if (responseText != null) {
logStateNotifier.logLlmText(responseText);
chatStateNotifier.appendToMessage(llmMessage.id, responseText);
}
}
This code:
- Checks if the LLM response contains any function calls
- For each function call, invokes your
handleFunctionCallmethod with the function name and arguments - Collects the results of each function call
- Sends these results back to the LLM using
Content.functionResponses - Processes the LLM's response to the function results
- Updates the UI with the final response text
This creates a round trip flow:
- User → LLM: Requests a color
- LLM → App: Function calls with parameters
- App → User: New color displayed
- App → LLM: Function results
- LLM → User: Final response incorporating function results
Generate Riverpod code
Run the build runner command to generate the needed Riverpod code:
dart run build_runner build --delete-conflicting-outputs
Run and test the complete flow
Now run your application:
flutter run -d DEVICE

Try entering various color descriptions:
- "I'd like a deep crimson red"
- "Show me a calming sky blue"
- "Give me the color of fresh mint leaves"
- "I want to see a warm sunset orange"
- "Make it a rich royal purple"
Now you should see:
- Your message appearing in the chat interface
- Gemini's response appearing in the chat
- Function calls being logged in the log panel
- Function results being logged immediately after
- The color rectangle updating to display the described color
- RGB values updating to show the new color's components
- Gemini's final response appearing, often commenting on the color that was set
The log panel provides insight into what's happening behind the scenes. You'll see:
- The exact function calls Gemini is making
- The parameters it's choosing for each RGB value
- The results your function is returning
- The follow-up responses from Gemini
The color state notifier
The colorStateNotifier you're using to update colors is part of the colorist_ui package. It manages:
- The current color displayed in the UI
- The color history (last 10 colors)
- Notification of state changes to UI components
When you call updateColor with new RGB values, it:
- Creates a new
ColorDataobject with the provided values - Updates the current color in the app state
- Adds the color to the history
- Triggers UI updates through Riverpod's state management
The UI components in the colorist_ui package watch this state and automatically update when it changes, creating a reactive experience.
Understanding error handling
Your implementation includes robust error handling:
- Try-catch block : Wraps all LLM interactions to catch any exceptions
- Error logging : Records errors in the log panel with stack traces
- User feedback : Provides a friendly error message in the chat
- State cleanup : Finalizes the message state even if an error occurs
This ensures the app remains stable and provides appropriate feedback even when issues occur with the LLM service or function execution.
The power of function calling for user experience
What you've accomplished here demonstrates how LLMs can create powerful natural interfaces:
- Natural language interface : Users express intent in everyday language
- Intelligent interpretation : The LLM translates vague descriptions into precise values
- Direct manipulation : The UI updates in response to natural language
- Contextual responses : The LLM provides conversational context about the changes
- Low cognitive load : Users don't need to understand RGB values or color theory
This pattern of using LLM function calling to bridge natural language and UI actions can be extended to countless other domains beyond color selection.
بعدش چی؟
In the next step, you'll enhance the user experience by implementing streaming responses. Rather than waiting for the complete response, you'll process text chunks and function calls as they are received, creating a more responsive and engaging application.
Troubleshooting
Function call issues
If Gemini isn't calling your functions or parameters are incorrect:
- Verify your function declaration matches what's described in the system prompt
- Check that parameter names and types are consistent
- Ensure your system prompt explicitly instructs the LLM to use the tool
- Verify the function name in your handler matches exactly what's in the declaration
- Examine the log panel for detailed information on function calls
Function response issues
If function results aren't being properly sent back to the LLM:
- Check that your function returns a properly formatted Map
- Verify that the Content.functionResponses is being constructed correctly
- Look for any errors in the log related to function responses
- Ensure you're using the same chat session for the response
Color display issues
If colors aren't displaying correctly:
- Ensure RGB values are properly converted to doubles (LLM might send them as integers)
- Verify that values are in the expected range (0.0 to 1.0)
- Check that the color state notifier is being called correctly
- Examine the log for the exact values being passed to the function
General problems
For general issues:
- Examine the logs for errors or warnings
- Verify Firebase AI Logic connectivity
- Check for any type mismatches in function parameters
- Ensure all Riverpod generated code is up to date
Key concepts learned
- Implementing a complete function calling pipeline in Flutter
- Creating full communication between an LLM and your application
- Processing structured data from LLM responses
- Sending function results back to the LLM for incorporation into responses
- Using the log panel to gain visibility into LLM-application interactions
- Connecting natural language inputs to concrete UI changes
With this step complete, your app now demonstrates one of the most powerful patterns for LLM integration: translating natural language inputs into concrete UI actions, while maintaining a coherent conversation that acknowledges these actions. This creates an intuitive, conversational interface that feels magical to users.
7. Streaming responses for better UX
In this step, you'll enhance the user experience by implementing streaming responses from Gemini. Instead of waiting for the entire response to be generated, you'll process text chunks and function calls as they are received, creating a more responsive and engaging application.
What you'll cover in this step
- The importance of streaming for LLM-powered applications
- Implementing streaming LLM responses in a Flutter application
- Processing partial text chunks as they arrive from the API
- Managing conversation state to prevent message conflicts
- Handling function calls in streaming responses
- Creating visual indicators for in-progress responses
Why streaming matters for LLM applications
Before implementing, let's understand why streaming responses are crucial for creating excellent user experiences with LLMs:
Improved user experience
Streaming responses provide several significant user experience benefits:
- Reduced perceived latency : Users see text start appearing immediately (typically within 100-300ms), rather than waiting several seconds for a complete response. This perception of immediacy dramatically improves user satisfaction.
- Natural conversational rhythm : The gradual appearance of text mimics how humans communicate, creating a more natural dialogue experience.
- Progressive information processing : Users can begin processing information as it arrives, rather than being overwhelmed by a large block of text all at once.
- Opportunity for early interruption : In a full application, users could potentially interrupt or redirect the LLM if they see it going in an unhelpful direction.
- Visual confirmation of activity : The streaming text provides immediate feedback that the system is working, reducing uncertainty.
Technical advantages
Beyond UX improvements, streaming offers technical benefits:
- Early function execution : Function calls can be detected and executed as soon as they appear in the stream, without waiting for the complete response.
- Incremental UI updates : You can update your UI progressively as new information arrives, creating a more dynamic experience.
- Conversation state management : Streaming provides clear signals about when responses are complete vs. still in progress, enabling better state management.
- Reduced timeout risks : With non-streaming responses, long-running generations risk connection timeouts. Streaming establishes the connection early and maintains it.
For your Colorist app, implementing streaming means users will see both text responses and color changes appearing more promptly, creating a significantly more responsive experience.
Add conversation state management
First, let's add a state provider to track whether the app is currently handling a streaming response. Update your lib/services/gemini_chat_service.dart file:
lib/services/gemini_chat_service.dart
import 'dart:async';
import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../providers/gemini.dart';
import 'gemini_tools.dart';
part 'gemini_chat_service.g.dart';
class ConversationStateNotifier extends Notifier<ConversationState> { // Add from here...
@override
ConversationState build() => ConversationState.idle;
void busy() {
state = ConversationState.busy;
}
void idle() {
state = ConversationState.idle;
}
}
final conversationStateProvider =
NotifierProvider<ConversationStateNotifier, ConversationState>(
ConversationStateNotifier.new,
); // To here.
class GeminiChatService {
GeminiChatService(this.ref);
final Ref ref;
Future<void> sendMessage(String message) async {
final chatSession = await ref.read(chatSessionProvider.future);
final conversationState = ref.read(conversationStateProvider); // Add this line
final chatStateNotifier = ref.read(chatStateProvider.notifier);
final logStateNotifier = ref.read(logStateProvider.notifier);
if (conversationState == ConversationState.busy) { // Add from here...
logStateNotifier.logWarning(
"Can't send a message while a conversation is in progress",
);
throw Exception(
"Can't send a message while a conversation is in progress",
);
}
final conversationStateNotifier = ref.read(
conversationStateProvider.notifier,
);
conversationStateNotifier.busy(); // To here.
chatStateNotifier.addUserMessage(message);
logStateNotifier.logUserText(message);
final llmMessage = chatStateNotifier.createLlmMessage();
try { // Modify from here...
final responseStream = chatSession.sendMessageStream(
Content.text(message),
);
await for (final block in responseStream) {
await _processBlock(block, llmMessage.id);
} // To here.
} catch (e, st) {
logStateNotifier.logError(e, st: st);
chatStateNotifier.appendToMessage(
llmMessage.id,
"\nI'm sorry, I encountered an error processing your request. "
"Please try again.",
);
} finally {
chatStateNotifier.finalizeMessage(llmMessage.id);
conversationStateNotifier.idle(); // Add this line.
}
}
Future<void> _processBlock( // Add from here...
GenerateContentResponse block,
String llmMessageId,
) async {
final chatSession = await ref.read(chatSessionProvider.future);
final chatStateNotifier = ref.read(chatStateProvider.notifier);
final logStateNotifier = ref.read(logStateProvider.notifier);
final blockText = block.text;
if (blockText != null) {
logStateNotifier.logLlmText(blockText);
chatStateNotifier.appendToMessage(llmMessageId, blockText);
}
if (block.functionCalls.isNotEmpty) {
final geminiTools = ref.read(geminiToolsProvider);
final responseStream = chatSession.sendMessageStream(
Content.functionResponses([
for (final functionCall in block.functionCalls)
FunctionResponse(
functionCall.name,
geminiTools.handleFunctionCall(
functionCall.name,
functionCall.args,
),
),
]),
);
await for (final response in responseStream) {
final responseText = response.text;
if (responseText != null) {
logStateNotifier.logLlmText(responseText);
chatStateNotifier.appendToMessage(llmMessageId, responseText);
}
}
}
} // To here.
}
@Riverpod(keepAlive: true)
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);
Understanding the streaming implementation
Let's break down what this code does:
- Conversation state tracking :
- A
conversationStateProvidertracks whether the app is currently processing a response - The state transitions from
idle→busywhile processing, then back toidle - This prevents multiple concurrent requests that could conflict
- A
- Stream initialization :
-
sendMessageStream()returns a Stream of response chunks instead of aFuturewith the complete response - Each chunk may contain text, function calls, or both
-
- Progressive processing :
-
await forprocesses each chunk as it arrives in real-time - Text is appended to the UI immediately, creating the streaming effect
- Function calls are executed as soon as they're detected
-
- Function call handling :
- When a function call is detected in a chunk, it's executed immediately
- Results are sent back to the LLM through another streaming call
- The LLM's response to these results is also processed in a streaming fashion
- Error handling and cleanup :
-
try/catchprovides robust error handling - The
finallyblock ensures conversation state is reset properly - Message is always finalized, even if errors occur
-
This implementation creates a responsive, reliable streaming experience while maintaining proper conversation state.
Update the main screen to connect conversation state
Modify your lib/main.dart file to pass the conversation state to the main screen:
lib/main.dart
import 'package:colorist_ui/colorist_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'providers/gemini.dart';
import 'services/gemini_chat_service.dart';
void main() async {
runApp(ProviderScope(child: MainApp()));
}
class MainApp extends ConsumerWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final model = ref.watch(geminiModelProvider);
final conversationState = ref.watch(conversationStateProvider); // Add this line
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: model.when(
data: (data) => MainScreen(
conversationState: conversationState, // And this line
sendMessage: (text) {
ref.read(geminiChatServiceProvider).sendMessage(text);
},
),
loading: () => LoadingScreen(message: 'Initializing Gemini Model'),
error: (err, st) => ErrorScreen(error: err),
),
);
}
}
The key change here is passing the conversationState to the MainScreen widget. The MainScreen (provided by the colorist_ui package) will use this state to disable the text input while a response is being processed.
This creates a cohesive user experience where the UI reflects the current state of the conversation.
Generate Riverpod code
Run the build runner command to generate the needed Riverpod code:
dart run build_runner build --delete-conflicting-outputs
Run and test streaming responses
Run your application:
flutter run -d DEVICE

Now try testing the streaming behavior with various color descriptions. Try descriptions like:
- "Show me the deep teal color of the ocean at twilight"
- "I'd like to see a vibrant coral that reminds me of tropical flowers"
- "Create a muted olive green like old army fatigues"
The streaming technical flow in detail
Let's examine exactly what happens when streaming a response:
Connection establishment
When you call sendMessageStream() , the following happens:
- The app establishes a connection to the Firebase AI Logic service
- The user request is sent to the service
- The server begins processing the request
- The stream connection remains open, ready to transmit chunks
Chunk transmission
As Gemini generates content, chunks are sent through the stream:
- The server sends text chunks as they're generated (typically a few words or sentences)
- When Gemini decides to make a function call, it sends the function call information
- Additional text chunks may follow function calls
- The stream continues until the generation is complete
Progressive processing
Your app processes each chunk incrementally:
- Each text chunk is appended to the existing response
- Function calls are executed as soon as they're detected
- The UI updates in real-time with both text and function results
- State is tracked to show the response is still streaming
Stream completion
When the generation is complete:
- The stream is closed by the server
- Your
await forloop exits naturally - The message is marked as complete
- The conversation state is set back to idle
- The UI updates to reflect the completed state
Streaming vs. non-streaming comparison
To better understand the benefits of streaming, let's compare streaming vs. non-streaming approaches:
جنبه | Non-Streaming | پخش جریانی |
Perceived latency | User sees nothing until complete response is ready | User sees first words within milliseconds |
تجربه کاربری | Long wait followed by sudden text appearance | Natural, progressive text appearance |
State management | Simpler (messages are either pending or complete) | More complex (messages can be in a streaming state) |
Function execution | Occurs only after complete response | Occurs during response generation |
Implementation complexity | Simpler to implement | Requires additional state management |
Error recovery | All-or-nothing response | Partial responses may still be useful |
Code complexity | Less complex | More complex due to stream handling |
For an application like Colorist, the UX benefits of streaming outweigh the implementation complexity, especially for color interpretations that might take several seconds to generate.
Best practices for streaming UX
When implementing streaming in your own LLM applications, consider these best practices:
- Clear visual indicators : Always provide clear visual cues that distinguish streaming vs. complete messages
- Input blocking : Disable user input during streaming to prevent multiple overlapping requests
- Error recovery : Design your UI to handle graceful recovery if streaming is interrupted
- State transitions : Ensure smooth transitions between idle, streaming, and complete states
- Progress visualization : Consider subtle animations or indicators that show active processing
- Cancellation options : In a complete app, provide ways for users to cancel in-progress generations
- Function result integration : Design your UI to handle function results appearing mid-stream
- Performance optimization : Minimize UI rebuilds during rapid stream updates
The colorist_ui package implements many of these best practices for you, but they're important considerations for any streaming LLM implementation.
بعدش چی؟
In the next step, you'll implement LLM synchronization by notifying Gemini when users select colors from history. This will create a more cohesive experience where the LLM is aware of user-initiated changes to the application state.
Troubleshooting
Stream processing issues
If you encounter issues with stream processing:
- Symptoms : Partial responses, missing text, or abrupt stream termination
- Solution : Check network connectivity and ensure proper async/await patterns in your code
- Diagnosis : Examine the log panel for error messages or warnings related to stream processing
- Fix : Ensure all stream processing uses proper error handling with
try/catchblocks
Missing function calls
If function calls aren't being detected in the stream:
- Symptoms : Text appears but colors don't update, or log shows no function calls
- Solution : Verify the system prompt's instructions about using function calls
- Diagnosis : Check the log panel to see if function calls are being received
- Fix : Adjust your system prompt to more explicitly instruct the LLM to use the
set_colortool
General error handling
For any other issues:
- Step 1 : Check the log panel for error messages
- Step 2 : Verify Firebase AI Logic connectivity
- Step 3 : Ensure all Riverpod generated code is up to date
- Step 4 : Review the streaming implementation for any missing await statements
Key concepts learned
- Implementing streaming responses with the Gemini API for more responsive UX
- Managing conversation state to handle streaming interactions properly
- Processing real-time text and function calls as they arrive
- Creating responsive UIs that update incrementally during streaming
- Handling concurrent streams with proper async patterns
- Providing appropriate visual feedback during streaming responses
By implementing streaming, you've significantly enhanced the user experience of your Colorist app, creating a more responsive, engaging interface that feels truly conversational.
8. LLM Context Synchronization
In this bonus step, you'll implement LLM Context Synchronization by notifying Gemini when users select colors from history. This creates a more cohesive experience where the LLM is aware of user actions in the interface, not just their explicit messages.
What you'll cover in this step
- Creating LLM Context Synchronization between your UI and the LLM
- Serializing UI events into context the LLM can understand
- Updating conversation context based on user actions
- Creating a coherent experience across different interaction methods
- Enhancing LLM context awareness beyond explicit chat messages
Understanding LLM Context Synchronization
Traditional chatbots only respond to explicit user messages, creating a disconnect when users interact with the app through other means. LLM Context Synchronization addresses this limitation:
Why LLM Context Synchronization matters
When users interact with your app through UI elements (like selecting a color from history), the LLM has no way of knowing what happened unless you explicitly tell it. LLM Context Synchronization:
- Maintains context : Keeps the LLM informed about all relevant user actions
- Creates coherence : Produces a cohesive experience where the LLM acknowledges UI interactions
- Enhances intelligence : Allows the LLM to respond appropriately to all user actions
- Improves user experience : Makes the entire application feel more integrated and responsive
- Reduces user effort : Eliminates the need for users to manually explain their UI actions
In your Colorist app, when a user selects a color from history, you want Gemini to acknowledge this action and comment intelligently about the selected color, maintaining the illusion of a seamless, aware assistant.
Update the Gemini chat service for color selection notifications
First, you'll add a method to the GeminiChatService to notify the LLM when a user selects a color from history. Update your lib/services/gemini_chat_service.dart file:
lib/services/gemini_chat_service.dart
import 'dart:async';
import 'dart:convert'; // Add this import
import 'package:colorist_ui/colorist_ui.dart';
import 'package:firebase_ai/firebase_ai.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../providers/gemini.dart';
import 'gemini_tools.dart';
part 'gemini_chat_service.g.dart';
class ConversationStateNotifier extends Notifier<ConversationState> {
@override
ConversationState build() => ConversationState.idle;
void busy() {
state = ConversationState.busy;
}
void idle() {
state = ConversationState.idle;
}
}
final conversationStateProvider =
NotifierProvider<ConversationStateNotifier, ConversationState>(
ConversationStateNotifier.new,
);
class GeminiChatService {
GeminiChatService(this.ref);
final Ref ref;
Future<void> notifyColorSelection(ColorData color) => sendMessage( // Add from here...
'User selected color from history: ${json.encode(color.toLLMContextMap())}',
); // To here.
Future<void> sendMessage(String message) async {
final chatSession = await ref.read(chatSessionProvider.future);
final conversationState = ref.read(conversationStateProvider);
final chatStateNotifier = ref.read(chatStateProvider.notifier);
final logStateNotifier = ref.read(logStateProvider.notifier);
if (conversationState == ConversationState.busy) {
logStateNotifier.logWarning(
"Can't send a message while a conversation is in progress",
);
throw Exception(
"Can't send a message while a conversation is in progress",
);
}
final conversationStateNotifier = ref.read(
conversationStateProvider.notifier,
);
conversationStateNotifier.busy();
chatStateNotifier.addUserMessage(message);
logStateNotifier.logUserText(message);
final llmMessage = chatStateNotifier.createLlmMessage();
try {
final responseStream = chatSession.sendMessageStream(
Content.text(message),
);
await for (final block in responseStream) {
await _processBlock(block, llmMessage.id);
}
} catch (e, st) {
logStateNotifier.logError(e, st: st);
chatStateNotifier.appendToMessage(
llmMessage.id,
"\nI'm sorry, I encountered an error processing your request. "
"Please try again.",
);
} finally {
chatStateNotifier.finalizeMessage(llmMessage.id);
conversationStateNotifier.idle();
}
}
Future<void> _processBlock(
GenerateContentResponse block,
String llmMessageId,
) async {
final chatSession = await ref.read(chatSessionProvider.future);
final chatStateNotifier = ref.read(chatStateProvider.notifier);
final logStateNotifier = ref.read(logStateProvider.notifier);
final blockText = block.text;
if (blockText != null) {
logStateNotifier.logLlmText(blockText);
chatStateNotifier.appendToMessage(llmMessageId, blockText);
}
if (block.functionCalls.isNotEmpty) {
final geminiTools = ref.read(geminiToolsProvider);
final responseStream = chatSession.sendMessageStream(
Content.functionResponses([
for (final functionCall in block.functionCalls)
FunctionResponse(
functionCall.name,
geminiTools.handleFunctionCall(
functionCall.name,
functionCall.args,
),
),
]),
);
await for (final response in responseStream) {
final responseText = response.text;
if (responseText != null) {
logStateNotifier.logLlmText(responseText);
chatStateNotifier.appendToMessage(llmMessageId, responseText);
}
}
}
}
}
@Riverpod(keepAlive: true)
GeminiChatService geminiChatService(Ref ref) => GeminiChatService(ref);
The key addition is the notifyColorSelection method, which:
- Takes a
ColorDataobject representing the selected color - Encodes it to a JSON format that can be included in a message
- Sends a specially formatted message to the LLM indicating a user selection
- Reuses the existing
sendMessagemethod to handle the notification
This approach avoids duplication by utilizing your existing message handling infrastructure.
Update main app to connect color selection notifications
Now, modify your lib/main.dart file to pass the color selection notification function to the main screen:
lib/main.dart
import 'package:colorist_ui/colorist_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'providers/gemini.dart';
import 'services/gemini_chat_service.dart';
void main() async {
runApp(ProviderScope(child: MainApp()));
}
class MainApp extends ConsumerWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final model = ref.watch(geminiModelProvider);
final conversationState = ref.watch(conversationStateProvider);
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: model.when(
data: (data) => MainScreen(
conversationState: conversationState,
notifyColorSelection: (color) { // Add from here...
ref.read(geminiChatServiceProvider).notifyColorSelection(color);
}, // To here.
sendMessage: (text) {
ref.read(geminiChatServiceProvider).sendMessage(text);
},
),
loading: () => LoadingScreen(message: 'Initializing Gemini Model'),
error: (err, st) => ErrorScreen(error: err),
),
);
}
}
The key change is adding the notifyColorSelection callback, which connects the UI event (selecting a color from history) to the LLM notification system.
Update the system prompt
Now, you need to update your system prompt to instruct the LLM on how to respond to color selection notifications. Modify your assets/system_prompt.md file:
assets/system_prompt.md
# Colorist System Prompt
You are a color expert assistant integrated into a desktop app called Colorist. Your job is to interpret natural language color descriptions and set the appropriate color values using a specialized tool.
## Your Capabilities
You are knowledgeable about colors, color theory, and how to translate natural language descriptions into specific RGB values. You have access to the following tool:
`set_color` - Sets the RGB values for the color display based on a description
## How to Respond to User Inputs
When users describe a color:
1. First, acknowledge their color description with a brief, friendly response
2. Interpret what RGB values would best represent that color description
3. Use the `set_color` tool to set those values (all values should be between 0.0 and 1.0)
4. After setting the color, provide a brief explanation of your interpretation
Example:
User: "I want a sunset orange"
You: "Sunset orange is a warm, vibrant color that captures the golden-red hues of the setting sun. It combines a strong red component with moderate orange tones."
[Then you would call the set_color tool with approximately: red=1.0, green=0.5, blue=0.25]
After the tool call: "I've set a warm orange with strong red, moderate green, and minimal blue components that is reminiscent of the sun low on the horizon."
## When Descriptions are Unclear
If a color description is ambiguous or unclear, please ask the user clarifying questions, one at a time.
## When Users Select Historical Colors
Sometimes, the user will manually select a color from the history panel. When this happens, you'll receive a notification about this selection that includes details about the color. Acknowledge this selection with a brief response that recognizes what they've done and comments on the selected color.
Example notification:
User: "User selected color from history: {red: 0.2, green: 0.5, blue: 0.8, hexCode: #3380CC}"
You: "I see you've selected an ocean blue from your history. This tranquil blue with a moderate intensity has a calming, professional quality to it. Would you like to explore similar shades or create a contrasting color?"
## Important Guidelines
- Always keep RGB values between 0.0 and 1.0
- Provide thoughtful, knowledgeable responses about colors
- When possible, include color psychology, associations, or interesting facts about colors
- Be conversational and engaging in your responses
- Focus on being helpful and accurate with your color interpretations
The key addition is the "When Users Select Historical Colors" section, which:
- Explains the concept of history selection notifications to the LLM
- Provides an example of what these notifications look like
- Shows an example of an appropriate response
- Sets expectations for acknowledging the selection and commenting on the color
This helps the LLM understand how to respond appropriately to these special messages.
Generate Riverpod Code
Run the build runner command to generate the needed Riverpod code:
dart run build_runner build --delete-conflicting-outputs
Run and test LLM Context Synchronization
Run your application:
flutter run -d DEVICE

Testing the LLM Context Synchronization involves:
- First, generate a few colors by describing them in the chat
- "Show me a vibrant purple"
- "I'd like a forest green"
- "Give me a bright red"
- Then, click on one of the color thumbnails in the history strip
You should observe:
- The selected color appears in the main display
- A user message appears in the chat indicating the color selection
- The LLM responds by acknowledging the selection and commenting on the color
- The entire interaction feels natural and cohesive
This creates a seamless experience where the LLM is aware of and responds appropriately to both direct messages and UI interactions.
How LLM Context Synchronization works
Let's explore the technical details of how this synchronization works:
Data Flow
- User action : User clicks a color in the history strip
- UI event : The
MainScreenwidget detects this selection - Callback execution : The
notifyColorSelectioncallback is triggered - Message creation : A specially formatted message is created with the color data
- LLM processing : The message is sent to Gemini, which recognizes the format
- Contextual response : Gemini responds appropriately based on the system prompt
- UI update : The response appears in the chat, creating a cohesive experience
Data serialization
A key aspect of this approach is how you serialize the color data:
'User selected color from history: ${json.encode(color.toLLMContextMap())}'
The toLLMContextMap() method (provided by the colorist_ui package) converts a ColorData object into a map with key properties that the LLM can understand. This typically includes:
- RGB values (red, green, blue)
- Hex code representation
- Any name or description associated with the color
By formatting this data consistently and including it in the message, you ensure the LLM has all the information it needs to respond appropriately.
Broader applications of LLM Context Synchronization
This pattern of notifying the LLM about UI events has numerous applications beyond color selection:
Other use cases
- Filter changes : Notify the LLM when users apply filters to data
- Navigation events : Inform the LLM when users navigate to different sections
- Selection changes : Update the LLM when users select items from lists or grids
- Preference updates : Tell the LLM when users change settings or preferences
- Data manipulation : Notify the LLM when users add, edit, or delete data
In each case, the pattern remains the same:
- Detect the UI event
- Serialize relevant data
- Send a specially formatted notification to the LLM
- Guide the LLM to respond appropriately through the system prompt
Best practices for LLM Context Synchronization
Based on your implementation, here are some best practices for effective LLM Context Synchronization:
1. Consistent formatting
Use a consistent format for notifications so the LLM can easily identify them:
"User [action] [object]: [structured data]"
2. Rich context
Include enough detail in notifications for the LLM to respond intelligently. For colors, this means RGB values, hex codes, and any other relevant properties.
3. Clear instructions
Provide explicit instructions in the system prompt about how to handle notifications, ideally with examples.
4. Natural integration
Design notifications to flow naturally in the conversation, not as technical interruptions.
5. Selective notification
Only notify the LLM about actions that are relevant to the conversation. Not every UI event needs to be communicated.
Troubleshooting
Notification issues
If the LLM isn't responding properly to color selections:
- Check that the notification message format matches what's described in the system prompt
- Verify that the color data is being properly serialized
- Ensure the system prompt has clear instructions for handling selections
- Look for any errors in the chat service when sending notifications
Context management
If the LLM seems to lose context:
- Check that the chat session is being maintained properly
- Verify that conversation states transition correctly
- Ensure that notifications are being sent through the same chat session
General problems
For general issues:
- Examine the logs for errors or warnings
- Verify Firebase AI Logic connectivity
- Check for any type mismatches in function parameters
- Ensure all Riverpod generated code is up to date
Key concepts learned
- Creating LLM Context Synchronization between UI and LLM
- Serializing UI events into LLM-friendly context
- Guiding LLM behavior for different interaction patterns
- Creating a cohesive experience across message and non-message interactions
- Enhancing LLM awareness of the broader application state
By implementing LLM Context Synchronization, you've created a truly integrated experience where the LLM feels like an aware, responsive assistant rather than just a text generator. This pattern can be applied to countless other applications to create more natural, intuitive AI-powered interfaces.
۹. تبریک میگویم!
You've successfully completed the Colorist codelab! 🎉
What you've built
You've created a fully functional Flutter application that integrates Google's Gemini API to interpret natural language color descriptions. Your app can now:
- Process natural language descriptions like "sunset orange" or "deep ocean blue"
- Use Gemini to intelligently translate these descriptions into RGB values
- Display the interpreted colors in real-time with streaming responses
- Handle user interactions through both chat and UI elements
- Maintain contextual awareness across different interaction methods
از اینجا به کجا برویم؟
Now that you've mastered the basics of integrating Gemini with Flutter, here are some ways to continue your journey:
Enhance your Colorist app
- Color palettes : Add functionality to generate complementary or matching color schemes
- Voice input : Integrate speech recognition for verbal color descriptions
- History management : Add options to name, organize, and export color sets
- Custom prompting : Create an interface for users to customize system prompts
- Advanced analytics : Track which descriptions work best or cause difficulties
Explore more Gemini features
- Multimodal inputs : Add image inputs to extract colors from photos
- Content generation : Use Gemini to generate color-related content like descriptions or stories
- Function calling enhancements : Create more complex tool integrations with multiple functions
- Safety settings : Explore different safety settings and their impact on responses
Apply these patterns to other domains
- Document analysis : Create apps that can understand and analyze documents
- Creative writing assistance : Build writing tools with LLM-powered suggestions
- Task automation : Design apps that translate natural language into automated tasks
- Knowledge-based applications : Create expert systems in specific domains
منابع
Here are some valuable resources to continue your learning:
اسناد رسمی
Prompting course and guide
جامعه
Observable Flutter Agentic series
In expisode #59, Craig Labenz and Andrew Brogden explore this codelab, highlighting interesting parts of the app build.
In episode #60, join Craig and Andrew again as they extend the codelab app with new capabilities and fight with making LLMs do as they are told.
In episode #61, Craig is joined by Chris Sells to have a fresh take at analysing news headlines and generates corresponding images.
بازخورد
We'd love to hear about your experience with this codelab! Please consider providing feedback through:
Thank you for completing this codelab, and we hope you continue exploring the exciting possibilities at the intersection of Flutter and AI!