-
Notifications
You must be signed in to change notification settings - Fork 0
Example Chat App
Amayyas edited this page Dec 5, 2025
·
2 revisions
A complete example of building a chat application with Flutter AI SDK.
- Real-time streaming responses
- Multi-turn conversations
- Provider switching
- Error handling
- Message history
import 'package:flutter/material.dart';
import 'package:flutter_ai_sdk/flutter_ai_sdk.dart';
void main() {
runApp(const ChatApp());
}
class ChatApp extends StatelessWidget {
const ChatApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'AI Chat',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const ChatScreen(),
);
}
}
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final TextEditingController _controller = TextEditingController();
final ScrollController _scrollController = ScrollController();
final List<ChatMessage> _messages = [];
FlutterAI? _ai;
bool _isLoading = false;
String _currentResponse = '';
AIProvider _selectedProvider = AIProvider.openai;
@override
void initState() {
super.initState();
_initAI();
}
void _initAI() {
_ai?.dispose();
_ai = FlutterAI(
provider: _selectedProvider,
config: AIConfig(
apiKey: _getApiKey(_selectedProvider),
model: _getDefaultModel(_selectedProvider),
temperature: 0.7,
systemPrompt: 'You are a helpful and friendly assistant.',
),
);
}
String _getApiKey(AIProvider provider) {
// In production, use secure storage
switch (provider) {
case AIProvider.openai:
return const String.fromEnvironment('OPENAI_KEY');
case AIProvider.anthropic:
return const String.fromEnvironment('ANTHROPIC_KEY');
case AIProvider.googleAI:
return const String.fromEnvironment('GOOGLE_KEY');
}
}
String _getDefaultModel(AIProvider provider) {
switch (provider) {
case AIProvider.openai:
return 'gpt-4-turbo';
case AIProvider.anthropic:
return 'claude-3-5-sonnet-latest';
case AIProvider.googleAI:
return 'gemini-1.5-pro';
}
}
Future<void> _sendMessage(String text) async {
if (text.trim().isEmpty || _ai == null) return;
setState(() {
_messages.add(ChatMessage(
text: text,
isUser: true,
timestamp: DateTime.now(),
));
_isLoading = true;
_currentResponse = '';
});
_controller.clear();
_scrollToBottom();
try {
// Stream the response
await for (final chunk in _ai!.streamChat(text)) {
if (chunk.isDelta && chunk.delta != null) {
setState(() {
_currentResponse += chunk.delta!;
});
_scrollToBottom();
}
if (chunk.isDone) {
setState(() {
_messages.add(ChatMessage(
text: _currentResponse,
isUser: false,
timestamp: DateTime.now(),
tokens: chunk.usage?.totalTokens,
));
_currentResponse = '';
_isLoading = false;
});
}
}
} on AIAuthenticationError {
_showError('Invalid API key. Please check your configuration.');
} on AIRateLimitError catch (e) {
_showError('Rate limited. Retry in ${e.retryAfter?.inSeconds ?? 60}s');
} on AIError catch (e) {
_showError('Error: ${e.message}');
} catch (e) {
_showError('Unexpected error: $e');
} finally {
setState(() {
_isLoading = false;
_currentResponse = '';
});
}
}
void _showError(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
void _clearChat() {
setState(() {
_messages.clear();
});
_ai?.clearContext();
}
void _changeProvider(AIProvider provider) {
setState(() {
_selectedProvider = provider;
_messages.clear();
});
_initAI();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('AI Chat'),
actions: [
PopupMenuButton<AIProvider>(
icon: const Icon(Icons.swap_horiz),
tooltip: 'Change Provider',
onSelected: _changeProvider,
itemBuilder: (context) => [
const PopupMenuItem(
value: AIProvider.openai,
child: Text('OpenAI'),
),
const PopupMenuItem(
value: AIProvider.anthropic,
child: Text('Anthropic'),
),
const PopupMenuItem(
value: AIProvider.googleAI,
child: Text('Google AI'),
),
],
),
IconButton(
icon: const Icon(Icons.delete_outline),
tooltip: 'Clear Chat',
onPressed: _clearChat,
),
],
),
body: Column(
children: [
// Provider indicator
Container(
padding: const EdgeInsets.symmetric(vertical: 4),
color: Colors.grey[200],
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.smart_toy, size: 16),
const SizedBox(width: 8),
Text(
'Using: ${_selectedProvider.name}',
style: const TextStyle(fontSize: 12),
),
],
),
),
// Messages list
Expanded(
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(16),
itemCount: _messages.length + (_currentResponse.isNotEmpty ? 1 : 0),
itemBuilder: (context, index) {
if (index == _messages.length && _currentResponse.isNotEmpty) {
return MessageBubble(
message: ChatMessage(
text: _currentResponse,
isUser: false,
timestamp: DateTime.now(),
),
isStreaming: true,
);
}
return MessageBubble(message: _messages[index]);
},
),
),
// Loading indicator
if (_isLoading && _currentResponse.isEmpty)
const Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(width: 8),
Text('Thinking...'),
],
),
),
// Input field
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Type a message...',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onSubmitted: _isLoading ? null : _sendMessage,
enabled: !_isLoading,
),
),
const SizedBox(width: 8),
IconButton.filled(
icon: const Icon(Icons.send),
onPressed: _isLoading
? null
: () => _sendMessage(_controller.text),
),
],
),
),
],
),
);
}
@override
void dispose() {
_ai?.dispose();
_controller.dispose();
_scrollController.dispose();
super.dispose();
}
}
class ChatMessage {
final String text;
final bool isUser;
final DateTime timestamp;
final int? tokens;
ChatMessage({
required this.text,
required this.isUser,
required this.timestamp,
this.tokens,
});
}
class MessageBubble extends StatelessWidget {
final ChatMessage message;
final bool isStreaming;
const MessageBubble({
super.key,
required this.message,
this.isStreaming = false,
});
@override
Widget build(BuildContext context) {
return Align(
alignment: message.isUser
? Alignment.centerRight
: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.all(12),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.75,
),
decoration: BoxDecoration(
color: message.isUser
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableText(message.text),
if (isStreaming)
const Padding(
padding: EdgeInsets.only(top: 4),
child: SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
if (message.tokens != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'${message.tokens} tokens',
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
),
),
),
],
),
),
);
}
}void _changeProvider(AIProvider provider) {
setState(() {
_selectedProvider = provider;
_messages.clear();
});
_initAI();
}await for (final chunk in _ai!.streamChat(text)) {
if (chunk.isDelta && chunk.delta != null) {
setState(() {
_currentResponse += chunk.delta!;
});
}
}try {
// ... stream response
} on AIAuthenticationError {
_showError('Invalid API key.');
} on AIRateLimitError catch (e) {
_showError('Rate limited. Retry in ${e.retryAfter?.inSeconds}s');
}@override
void dispose() {
_ai?.dispose();
_controller.dispose();
_scrollController.dispose();
super.dispose();
}flutter run --dart-define=OPENAI_KEY=sk-... --dart-define=ANTHROPIC_KEY=sk-ant-... --dart-define=GOOGLE_KEY=AIza...- Streaming - Streaming details
- Providers - Provider comparison
- Error Handling - Error types
Getting Started
Core Concepts
Advanced Features
API Reference
Examples
Other