Skip to content

Example Chat App

Amayyas edited this page Jul 6, 2026 · 2 revisions

Example: Chat Application

A complete example of building a chat application with Flutter AI SDK.

Features

  • Real-time streaming responses
  • Multi-turn conversations
  • Provider switching
  • Error handling
  • Message history

Full Code

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-5.5';
      case AIProvider.anthropic:
        return 'claude-opus-4-8';
      case AIProvider.googleAI:
        return 'gemini-3.5-flash';
    }
  }

  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],
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }
}

Key Features Explained

1. Provider Switching

void _changeProvider(AIProvider provider) {
  setState(() {
    _selectedProvider = provider;
    _messages.clear();
  });
  _initAI();
}

2. Streaming Responses

await for (final chunk in _ai!.streamChat(text)) {
  if (chunk.isDelta && chunk.delta != null) {
    setState(() {
      _currentResponse += chunk.delta!;
    });
  }
}

3. Error Handling

try {
  // ... stream response
} on AIAuthenticationError {
  _showError('Invalid API key.');
} on AIRateLimitError catch (e) {
  _showError('Rate limited. Retry in ${e.retryAfter?.inSeconds}s');
}

4. Resource Cleanup

@override
void dispose() {
  _ai?.dispose();
  _controller.dispose();
  _scrollController.dispose();
  super.dispose();
}

Running the Example

flutter run --dart-define=OPENAI_KEY=sk-... --dart-define=ANTHROPIC_KEY=sk-ant-... --dart-define=GOOGLE_KEY=AIza...

Related

Clone this wiki locally