Introduction: In this tutorial, we’ll create a chat interface in Flutter that communicates with a Generative AI model using the google_generative_ai
package. We'll guide you through each step of the process, explaining the code along the way.
1. Generating API Key: Before we proceed, you’ll need an API key to use the Gemini AI model. Follow these steps to generate an API key:
- Visit the Google AI Studio API Key page.
- Sign in with your Google account if prompted.
- If you haven’t already created an API key, click on the “Create Key” button.
- Copy the generated API key.
- Replace
'YOUR_API_KEY'
with the copied API key in the_ChatWidgetState
class where it's defined asstatic const _apiKey = 'YOUR_API_KEY';
.
This step ensures that you have a valid API key to authenticate your requests to the Gemini AI model.
2. Setting up Dependencies: First, ensure you have the necessary dependencies listed in your pubspec.yaml
file:
dependencies:
flutter:
sdk: flutter
google_generative_ai: ^0.2.0
flutter_markdown: ^0.6.19
3. Importing Packages: Import the required packages in your Dart file:
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:google_generative_ai/google_generative_ai.dart';
4. Creating the Chat Screen: Create a stateful widget called ChatScreen
. This widget will serve as the main entry point for our chat interface.
class ChatScreen extends StatefulWidget {
const ChatScreen({Key? key, required this.title});
final String title; @override
State<ChatScreen> createState() => _ChatScreenState();
}
5. Implementing the Chat Screen State: In the _ChatScreenState
class, override the build
method to define the UI of the chat screen. Use a Scaffold
widget with an AppBar
and a ChatWidget
as the body.
class _ChatScreenState extends State<ChatScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: const ChatWidget(),
);
}
}
6. Creating the Chat Widget: Define a stateful widget called ChatWidget
. This widget will contain the chat messages and the input field.
class ChatWidget extends StatefulWidget {
const ChatWidget({Key? key});
@override
State<ChatWidget> createState() => _ChatWidgetState();
}
7. Implementing the Chat Widget State: Inside the _ChatWidgetState
class, initialize the necessary variables and controllers in the initState
method. Set up a GenerativeModel
and start a chat session.
class _ChatWidgetState extends State<ChatWidget> {
late final GenerativeModel _model;
late final ChatSession _chat;
final ScrollController _scrollController = ScrollController();
final TextEditingController _textController = TextEditingController();
final FocusNode _textFieldFocus = FocusNode();
bool _loading = false;
static const _apiKey = 'YOUR_API_KEY'; @override
void initState() {
super.initState();
_model = GenerativeModel(
model: 'gemini-pro',
apiKey: _apiKey,
);
_chat = _model.startChat();
}
8. Building the Chat UI: In the build
method of _ChatWidgetState
, define the UI for displaying chat messages and the input field. Use a ListView.builder
to display the chat history and a TextField
for user input.
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _apiKey.isNotEmpty
? ListView.builder(
controller: _scrollController,
itemBuilder: (context, idx) {
// Display chat messages using MessageWidget
},
itemCount: _chat.history.length,
)
: ListView(
children: const [
Text('No API key found. Please provide an API Key.'),
],
),
),
// Input field for sending messages
],
),
);
}
9. Sending Messages:
To implement the _sendChatMessage
method, we need to:
- Get the message from the text controller.
- Send the message to the Generative AI model using
_chat.sendMessage
. - Update the chat history with the response.
- Scroll to the latest message.
- Handle any errors that may occur during the process.
Here’s the code for implementing the _sendChatMessage
method:
Future<void> _sendChatMessage(String message) async {
setState(() {
_loading = true;
});
try {
var response = await _chat.sendMessage(Content.text(message));
var text = response.text; if (text == null) {
_showError('No response from API.');
return;
} else {
setState(() {
_loading = false;
_scrollDown();
});
}
} catch (e) {
_showError(e.toString());
setState(() {
_loading = false;
});
}
}
10. Displaying Chat Messages:
To create the MessageWidget
widget, we need to:
- Determine the alignment of the message based on whether it is from the user or the AI model.
- Format the message according to the desired UI design.
Here’s the code for creating the MessageWidget
widget:
class MessageWidget extends StatelessWidget {
final String text;
final bool isFromUser;
const MessageWidget({
Key? key,
required this.text,
required this.isFromUser,
}); @override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: isFromUser ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [
Flexible(
child: Container(
constraints: const BoxConstraints(maxWidth: 600),
decoration: BoxDecoration(
color: isFromUser ? Colors.blue : Colors.green,
borderRadius: BorderRadius.circular(18),
),
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
margin: const EdgeInsets.only(bottom: 8),
child: Text(
text,
style: TextStyle(color: Colors.white),
),
),
),
],
);
}
}
In the MessageWidget
widget, the isFromUser
parameter determines the alignment of the message. If isFromUser
is true, the message is aligned to the right (indicating it's from the user), otherwise, it's aligned to the left (indicating it's from the AI model). The message is displayed inside a Container
with rounded corners and a background color (blue for user messages and green for AI messages). The Text
widget inside the Container
displays the message text in white color.
Conclusion: In this tutorial, we’ve covered the process of building a chat interface in Flutter that communicates with a Generative AI model. We explained each step, from setting up dependencies to implementing functionality. With this knowledge, you can create interactive chat interfaces powered by Generative AI in your Flutter applications.
Final Code:
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:google_generative_ai/google_generative_ai.dart';
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key, required this.title}); final String title; @override
State<ChatScreen> createState() => _ChatScreenState();
}class _ChatScreenState extends State<ChatScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: const ChatWidget(),
);
}
}class ChatWidget extends StatefulWidget {
const ChatWidget({super.key}); @override
State<ChatWidget> createState() => _ChatWidgetState();
}class _ChatWidgetState extends State<ChatWidget> {
late final GenerativeModel _model;
late final ChatSession _chat;
final ScrollController _scrollController = ScrollController();
final TextEditingController _textController = TextEditingController();
final FocusNode _textFieldFocus = FocusNode();
bool _loading = false;
static const _apiKey = 'AIzaSyCNPXTgr6aDQyy2GJgunSUdaLJ7gF1rwpg'; @override
void initState() {
super.initState();
_model = GenerativeModel(
model: 'gemini-pro',
apiKey: _apiKey,
);
_chat = _model.startChat();
} void _scrollDown() {
WidgetsBinding.instance.addPostFrameCallback(
(_) => _scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(
milliseconds: 750,
),
curve: Curves.easeOutCirc,
),
);
} @override
Widget build(BuildContext context) {
var textFieldDecoration = InputDecoration(
contentPadding: const EdgeInsets.all(15),
hintText: 'Enter a prompt...',
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.secondary,
),
),
); return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _apiKey.isNotEmpty
? ListView.builder(
controller: _scrollController,
itemBuilder: (context, idx) {
var content = _chat.history.toList()[idx];
var text = content.parts
.whereType<TextPart>()
.map<String>((e) => e.text)
.join('');
return MessageWidget(
text: text,
isFromUser: content.role == 'user',
);
},
itemCount: _chat.history.length,
)
: ListView(
children: const [
Text('No API key found. Please provide an API Key.'),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 25,
horizontal: 15,
),
child: Row(
children: [
Expanded(
child: TextField(
autofocus: true,
focusNode: _textFieldFocus,
decoration: textFieldDecoration,
controller: _textController,
onSubmitted: (String value) {
_sendChatMessage(value);
},
),
),
const SizedBox.square(
dimension: 15,
),
if (!_loading)
IconButton(
onPressed: () async {
_sendChatMessage(_textController.text);
},
icon: Icon(
Icons.send,
color: Theme.of(context).colorScheme.primary,
),
)
else
const CircularProgressIndicator(),
],
),
),
],
),
);
} Future<void> _sendChatMessage(String message) async {
setState(() {
_loading = true;
}); try {
var response = await _chat.sendMessage(
Content.text(message),
);
var text = response.text; if (text == null) {
_showError('No response from API.');
return;
} else {
setState(() {
_loading = false;
_scrollDown();
});
}
} catch (e) {
_showError(e.toString());
setState(() {
_loading = false;
});
} finally {
_textController.clear();
setState(() {
_loading = false;
});
_textFieldFocus.requestFocus();
}
} void _showError(String message) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Something went wrong'),
content: SingleChildScrollView(
child: SelectableText(message),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('OK'),
)
],
);
},
);
}
}class MessageWidget extends StatelessWidget {
final String text;
final bool isFromUser; const MessageWidget({
super.key,
required this.text,
required this.isFromUser,
}); @override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment:
isFromUser ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [
Flexible(
child: Container(
constraints: const BoxConstraints(maxWidth: 600),
decoration: BoxDecoration(
color: isFromUser
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(18),
),
padding: const EdgeInsets.symmetric(
vertical: 15,
horizontal: 20,
),
margin: const EdgeInsets.only(bottom: 8),
child: MarkdownBody(
selectable: true,
data: text,
),
),
),
],
);
}
}