Skip to content

Instantly share code, notes, and snippets.

@devoncarew
Created May 13, 2025 20:56
Show Gist options
  • Save devoncarew/915fb37cb26377ee7b0d2c887c1bd80e to your computer and use it in GitHub Desktop.
Save devoncarew/915fb37cb26377ee7b0d2c887c1bd80e to your computer and use it in GitHub Desktop.
// Copyright 2024 the Dart project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:google_generative_ai/google_generative_ai.dart';
import 'package:url_launcher/link.dart';
void main() {
runApp(const GenerativeAISample());
}
class GenerativeAISample extends StatelessWidget {
const GenerativeAISample({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter + Generative AI',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
brightness: Brightness.dark,
seedColor: const Color.fromARGB(255, 171, 222, 244),
),
useMaterial3: true,
),
home: const ChatScreen(title: 'Flutter + Generative AI'),
);
}
}
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key, required this.title});
final String title;
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
String? apiKey;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: switch (apiKey) {
final providedKey? => ChatWidget(apiKey: providedKey),
_ => ApiKeyWidget(
onSubmitted: (key) {
setState(() => apiKey = key);
},
),
},
);
}
}
class ApiKeyWidget extends StatelessWidget {
ApiKeyWidget({required this.onSubmitted, super.key});
final ValueChanged<String> onSubmitted;
final TextEditingController _textController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'To use the Gemini API, you\'ll need an API key. '
'If you don\'t already have one, '
'create a key in Google AI Studio.',
),
const SizedBox(height: 8),
Link(
uri: Uri.https('makersuite.google.com', '/app/apikey'),
target: LinkTarget.blank,
builder:
(context, followLink) => TextButton(
onPressed: followLink,
child: const Text('Get an API Key'),
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: TextField(
decoration: textFieldDecoration(
context,
'Enter your API key',
),
controller: _textController,
onSubmitted: (value) {
onSubmitted(value);
},
),
),
const SizedBox(height: 8),
TextButton(
onPressed: () {
onSubmitted(_textController.value.text);
},
child: const Text('Submit'),
),
],
),
],
),
),
);
}
}
class ChatWidget extends StatefulWidget {
const ChatWidget({required this.apiKey, super.key});
final String apiKey;
@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(debugLabel: 'TextField');
bool _loading = false;
@override
void initState() {
super.initState();
_model = GenerativeModel(model: 'gemini-pro', apiKey: widget.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) {
final history = _chat.history.toList();
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ListView.builder(
controller: _scrollController,
itemBuilder: (context, idx) {
final content = history[idx];
final text = content.parts
.whereType<TextPart>()
.map<String>((e) => e.text)
.join('');
return MessageWidget(
text: text,
isFromUser: content.role == 'user',
);
},
itemCount: history.length,
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 15),
child: Row(
children: [
Expanded(
child: TextField(
autofocus: true,
focusNode: _textFieldFocus,
decoration: textFieldDecoration(
context,
'Enter a prompt...',
),
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 {
final response = await _chat.sendMessage(Content.text(message));
final text = response.text;
if (text == null) {
_showError('Empty response.');
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<void>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Something went wrong'),
content: SingleChildScrollView(child: Text(message)),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('OK'),
),
],
);
},
);
}
}
class MessageWidget extends StatelessWidget {
const MessageWidget({
super.key,
required this.text,
required this.isFromUser,
});
final String text;
final bool isFromUser;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment:
isFromUser ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [
Flexible(
child: Container(
constraints: const BoxConstraints(maxWidth: 480),
decoration: BoxDecoration(
color:
isFromUser
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(18),
),
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
margin: const EdgeInsets.only(bottom: 8),
child: MarkdownBody(data: text),
),
),
],
);
}
}
InputDecoration textFieldDecoration(BuildContext context, String hintText) =>
InputDecoration(
contentPadding: const EdgeInsets.all(15),
hintText: hintText,
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),
),
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment