Chat dengan Ai
Struktur Proyek
Pertama, sesuaikan struktur folder dan file berikut di dalam direktori lib/
Anda:
lib/
├── main.dart
|
├── models/
│ └── chat_message.dart
|
├── screens/
│ └── chat_screen.dart
|
├── services/
│ └── gemini_service.dart
|
└── widgets/
├── message_bubble.dart
└── message_input_bar.dart
Langkah 1: Persiapan (Sama seperti sebelumnya)
Dapatkan Gemini API Key dari Google AI Studio.
Buat file
.env
di root proyek dan isi dengan:GEMINI_API_KEY=API_KEY_ANDA
Daftarkan
.env
dipubspec.yaml
di bawahassets
:YAMLflutter: assets: - .env
Tambahkan dependensi di terminal:Bash
flutter pub add google_generative_ai flutter_dotenv flutter_markdown uuid
(Kita menambahkan
uuid
untuk memberikan ID unik pada setiap pesan, ini adalah praktik yang baik).
Langkah 2: Tulis Kode untuk Setiap File
Sekarang, mari kita isi setiap file dengan kodenya masing-masing.
📁 lib/models/chat_message.dart
File ini mendefinisikan struktur data untuk sebuah pesan chat.
Dart
// lib/models/chat_message.dart
class ChatMessage {
final String id;
final String text;
final bool isFromUser;
final bool isTyping; // Untuk menampilkan indikator loading
final bool isError; // Untuk menandai pesan error
ChatMessage({
required this.id,
required this.text,
required this.isFromUser,
this.isTyping = false,
this.isError = false,
});
}
Tujuan: Memisahkan model data dari UI dan logika, membuat kode lebih mudah dibaca dan dikelola.
📁 lib/services/gemini_service.dart
File ini bertanggung jawab untuk semua interaksi dengan Gemini API. File ini tidak tahu menahu tentang UI (Flutter).
Dart
// lib/services/gemini_service.dart
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:google_generative_ai/google_generative_ai.dart';
class GeminiService {
late final GenerativeModel _model;
late final ChatSession _chat;
// Inisialisasi model dan sesi chat
GeminiService() {
final apiKey = dotenv.env['GEMINI_API_KEY'];
if (apiKey == null) {
throw Exception("GEMINI_API_KEY not found in .env file");
}
_model = GenerativeModel(
model: 'gemini-1.5-flash',
apiKey: apiKey,
);
_chat = _model.startChat();
}
/// Mengirim pesan ke Gemini dan mengembalikan respons teks
Future<String> sendMessage(String prompt) async {
try {
final response = await _chat.sendMessage(Content.text(prompt));
final text = response.text;
if (text == null) {
throw Exception("Received null response from Gemini.");
}
return text;
} catch (e) {
// Melempar kembali error untuk ditangani oleh UI
print("Error sending message: $e");
rethrow;
}
}
}
Tujuan: Mengisolasi logika API. Jika Anda ingin beralih ke layanan AI lain, Anda hanya perlu mengubah file ini.
📁 lib/widgets/message_bubble.dart
Widget ini hanya untuk menampilkan satu gelembung pesan.
Dart
// lib/widgets/message_bubble.dart
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:gemini_chat_app/models/chat_message.dart';
class MessageBubble extends StatelessWidget {
final ChatMessage message;
const MessageBubble({super.key, required this.message});
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
// Menampilkan indikator loading jika pesan adalah 'typing'
if (message.isTyping) {
return Align(
alignment: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 8, left: 15, right: 15),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
decoration: BoxDecoration(
color: colors.surfaceVariant,
borderRadius: BorderRadius.circular(18),
),
child: const SizedBox(
width: 25,
height: 25,
child: CircularProgressIndicator(strokeWidth: 3),
),
),
);
}
// Menentukan warna dan alignment berdasarkan pengirim atau status error
Color bubbleColor;
if (message.isError) {
bubbleColor = colors.errorContainer;
} else if (message.isFromUser) {
bubbleColor = colors.primaryContainer;
} else {
bubbleColor = colors.surfaceVariant;
}
return Align(
alignment: message.isFromUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 8, left: 15, right: 15),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
decoration: BoxDecoration(
color: bubbleColor,
borderRadius: BorderRadius.circular(18),
),
child: MarkdownBody(
data: message.text,
selectable: true,
),
),
);
}
}
Tujuan: Membuat widget yang dapat digunakan kembali untuk tampilan pesan, membuat kode layar utama (chat_screen.dart
) lebih bersih.
📁 lib/widgets/message_input_bar.dart
Widget ini meng-enkapsulasi TextField
dan tombol kirim.
Dart
// lib/widgets/message_input_bar.dart
import 'package:flutter/material.dart';
class MessageInputBar extends StatelessWidget {
final TextEditingController controller;
final bool isLoading;
final VoidCallback onSend;
const MessageInputBar({
super.key,
required this.controller,
required this.isLoading,
required this.onSend,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15),
child: Row(
children: [
Expanded(
child: TextField(
controller: controller,
autofocus: true,
decoration: InputDecoration(
contentPadding: const EdgeInsets.all(15),
hintText: 'Ketik pesan...',
border: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(14)),
borderSide: BorderSide(color: Theme.of(context).colorScheme.secondary),
),
focusedBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(14)),
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
),
),
onSubmitted: (_) => isLoading ? null : onSend(),
),
),
const SizedBox.square(dimension: 15),
IconButton(
icon: Icon(Icons.send, color: Theme.of(context).colorScheme.primary),
onPressed: isLoading ? null : onSend,
),
],
),
);
}
}
Tujuan: Memisahkan logika input dari layout utama layar, membuatnya lebih mudah untuk diubah atau digunakan kembali di tempat lain.
📁 lib/screens/chat_screen.dart
File ini menjadi "konduktor" yang mengatur state dan menyatukan semua widget menjadi satu layar fungsional.
Dart
// lib/screens/chat_screen.dart
import 'package:flutter/material.dart';
import 'package:gemini_chat_app/models/chat_message.dart';
import 'package:gemini_chat_app/services/gemini_service.dart';
import 'package:gemini_chat_app/widgets/message_bubble.dart';
import 'package:gemini_chat_app/widgets/message_input_bar.dart';
import 'package:uuid/uuid.dart';
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final GeminiService _geminiService = GeminiService();
final TextEditingController _textController = TextEditingController();
final ScrollController _scrollController = ScrollController();
final List<ChatMessage> _messages = [];
bool _isLoading = false;
final Uuid _uuid = const Uuid();
void _scrollDown() {
WidgetsBinding.instance.addPostFrameCallback(
(_) => _scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
),
);
}
Future<void> _sendMessage() async {
if (_textController.text.isEmpty) return;
final userMessageText = _textController.text;
_textController.clear();
FocusScope.of(context).unfocus(); // Tutup keyboard
setState(() {
_isLoading = true;
// Tambahkan pesan pengguna
_messages.add(ChatMessage(
id: _uuid.v4(),
text: userMessageText,
isFromUser: true,
));
_scrollDown();
// Tambahkan indikator 'typing' dari AI
_messages.add(ChatMessage(
id: _uuid.v4(),
text: '...',
isFromUser: false,
isTyping: true,
));
_scrollDown();
});
try {
final responseText = await _geminiService.sendMessage(userMessageText);
setState(() {
// Hapus indikator 'typing'
_messages.removeWhere((msg) => msg.isTyping);
// Tambahkan respons AI
_messages.add(ChatMessage(
id: _uuid.v4(),
text: responseText,
isFromUser: false,
));
});
} catch (e) {
setState(() {
// Hapus indikator 'typing'
_messages.removeWhere((msg) => msg.isTyping);
// Tambahkan pesan error
_messages.add(ChatMessage(
id: _uuid.v4(),
text: 'Maaf, terjadi kesalahan. Coba lagi.',
isFromUser: false,
isError: true,
));
});
} finally {
setState(() {
_isLoading = false;
});
_scrollDown();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Gemini Clean Arch'),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount: _messages.length,
itemBuilder: (context, index) {
final message = _messages[index];
return MessageBubble(message: message);
},
),
),
MessageInputBar(
controller: _textController,
isLoading: _isLoading,
onSend: _sendMessage,
),
],
),
);
}
}
Tujuan: Mengelola state aplikasi (daftar pesan, status loading) dan menyusun UI dari berbagai widget terpisah.
📁 lib/main.dart
File main.dart
sekarang menjadi sangat sederhana. Tugasnya hanya menginisialisasi aplikasi dan menunjuk ke layar utama.
Dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:gemini_chat_app/screens/chat_screen.dart';
void main() async {
// Pastikan binding Flutter siap sebelum memuat dotenv
WidgetsFlutterBinding.ensureInitialized();
// Muat environment variables dari file .env
await dotenv.load(fileName: ".env");
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Gemini Chat App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: const ChatScreen(),
debugShowCheckedModeBanner: false,
);
}
}
Tujuan: Sebagai titik masuk (entry point) aplikasi yang bersih dan ringkas.
Langkah 3: Jalankan Aplikasi
link github
https://github.com/triyono777/flutter_supabase_notes_app/tree/fitur-ai
Last updated
Was this helpful?