Інтеграція AI з Flutter у 2026 році (Gemini API + Flutter + Riverpod)

Автор Nihev, Сьогодні в 01:05 PM

« попередня та - наступна тема »

Nihev

Вступ
У 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
Додатки до 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.7


flutter 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 та Theme
main.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