ВступУ 2026 році комбінація Flutter + AI вибухнула: за допомогою моделей Gemini Google (gemini-1.5-flash, gemini-2.0-experimental тощо) додавати обробку природної мови, аналіз зображень і чатботи до додатків Flutter стало дуже легко. Завдяки Impeller інтерфейс плавний, Riverpod забезпечує чисте керування станом.
У цьому посібнику ми створимо Додаток AI Chatbot з нуля:- Чат з Gemini API (текст + вхідне зображення)
- Керування станом з Riverpod 2.0+ (історія чату, завантаження, помилка)
- StreamBuilder для відповіді в реальному часі
- Темний режим + Material 3
- Резервне офлайн (простий локальний кеш)
ВимогиFlutter 3.35+ (Impeller за замовчуванням), Dart 3.5+
Отримати ключ Gemini API: https://aistudio.google.com/app/apikey (https://aistudio.google.com/app/apikey?referrer=grok.com)
Додатки до pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.5.1
riverpod_annotation: ^2.3.5
http: ^1.2.2
json_annotation: ^4.9.0
freezed_annotation: ^2.4.4
flutter_chat_ui: ^1.6.14 # для красивого інтерфейсу чату (необов'язково, але рекомендується)
dev_dependencies:
build_runner: ^2.4.13
riverpod_generator: ^2.4.3
json_serializable: ^6.8.0
freezed: ^2.5.7flutter pub run build_runner build --delete-conflicting-outputs
Структура проектуlib/
core/
constants.dart # API key, models
features/
chat/
data/ # api service
domain/ # models, entities
presentation/
providers/
screens/
main.dartСервіс Gemini API (data/remote/gemini_service.dart)import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'gemini_service.g.dart';
@riverpod
class GeminiService extends _$GeminiService {
@override
Future<String> build(String prompt, {String? imageBase64}) async {
final apiKey = 'YOUR_GEMINI_API_KEY'; // .env veya const'tan al
final url = Uri.parse(
'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=$apiKey'
);
final body = {
"contents": [
{
"parts": [
{"text": prompt},
if (imageBase64 != null)
{
"inlineData": {
"mimeType": "image/jpeg",
"data": imageBase64
}
}
]
}
],
"generationConfig": {
"temperature": 0.7,
"topK": 40,
"topP": 0.95,
"maxOutputTokens": 1024,
}
};
final response = await http.post(url, body: jsonEncode(body), headers: {
'Content-Type': 'application/json',
});
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['candidates'][0]['content']['parts'][0]['text'] ?? 'Відповідь не отримано';
} else {
throw Exception('Помилка Gemini API: ${response.statusCode} - ${response.body}');
}
}
}Freezed для immutable моделі:Модель чату та стан (domain/models/chat_message.dart)
import 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_message.freezed.dart';
part 'chat_message.g.dart';
@freezed
class ChatMessage with _$ChatMessage {
const factory ChatMessage({
required String text,
required bool isUser,
@Default(false) bool isLoading,
String? imageBase64, // опціональний вхід зображення
}) = _ChatMessage;
factory ChatMessage.fromJson(Map<String, dynamic> json) => _$ChatMessageFromJson(json);
}Провайдер Riverpod (presentation/providers/chat_provider.dart)import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/models/chat_message.dart';
import '../../data/gemini_service.dart';
part 'chat_provider.g.dart';
@riverpod
class ChatNotifier extends _$ChatNotifier {
@override
List<ChatMessage> build() => [];
Future<void> sendMessage(String text, {String? imageBase64}) async {
state = [...state, ChatMessage(text: text, isUser: true, imageBase64: imageBase64)];
try {
final response = await ref.read(geminiServiceProvider)(text, imageBase64: imageBase64);
state = [...state, ChatMessage(text: response, isUser: false)];
} catch (e) {
state = [...state, ChatMessage(text: 'Помилка: $e', isUser: false, isLoading: false)];
}
}
void clearChat() => state = [];
}Екран чату (presentation/screens/chat_screen.dart)З пакетом Flutter Chat UI для красивого вигляду (або кастомний ListView):
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_chat_ui/flutter_chat_ui.dart'; // красивий чат UI
import '../providers/chat_provider.dart';
class ChatScreen extends ConsumerWidget {
final _controller = TextEditingController();
@override
Widget build(BuildContext context, WidgetRef ref) {
final messages = ref.watch(chatNotifierProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Gemini AI Chatbot 2026'),
actions: [
IconButton(icon: const Icon(Icons.delete), onPressed: () => ref.read(chatNotifierProvider.notifier).clearChat(), )
],
),
body: Chat(
messages: messages.map((m) => ChatMessage(
text: m.text,
createdAt: DateTime.now(), // в реальності додати timestamp
author: Author(id: m.isUser ? 'user' : 'ai'),
type: m.isLoading ? MessageType.loading : MessageType.text,
)).toList(),
onSendPressed: (msg) {
ref.read(chatNotifierProvider.notifier).sendMessage(msg.text);
_controller.clear();
},
user: const Author(id: 'user'),
textController: _controller,
theme: const DefaultChatTheme(
inputBackgroundColor: Colors.blueGrey,
primaryColor: Colors.blue,
),
),
);
}
}Main та Thememain.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'presentation/screens/chat_screen.dart';
void main() async {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Gemini Chat',
theme: ThemeData.light(useMaterial3: true),
darkTheme: ThemeData.dark(useMaterial3: true),
themeMode: ThemeMode.system,
home: ChatScreen(),
);
}
}- Поради для просунутих (2026)
- Вхід зображення: image_picker + base64 encode
- Потокова відповідь: використовувати ендпоінт потокового Gemini (generateContentStream)
- Обмеження швидкості + повтор помилок
- Локальний кеш: зберігати історію чату з hive або shared_preferences
- Тест Impeller: flutter run --enable-impeller