Start a project

Building AI-Powered Flutter Apps with Google Gemini in 2025

Learn how to integrate Google's Gemini AI into your Flutter apps using the new AI Toolkit. Discover multi-turn conversations, streaming responses, and advanced AI features with practical examples.

Introduction to Google Gemini

Google's Gemini AI represents a breakthrough in multimodal AI capabilities, offering developers unprecedented power to build intelligent applications. As Flutter developers in 2025, we now have access to Gemini's advanced features through the official Google AI Dart SDK, making it easier than ever to create AI-powered mobile applications.

Google Gemini AI Integration

Gemini stands out from other AI models with its ability to understand and generate not just text, but also process images, audio, and even video content. This multimodal approach opens up exciting possibilities for Flutter developers looking to create more intuitive and intelligent user experiences.

Prerequisites and Setup

Before we dive into the implementation, let's ensure you have everything set up correctly. You'll need:

Getting Started with the Google AI Dart SDK

First, let's add the Google AI Dart SDK to your Flutter project. Open your pubspec.yaml file and add the following dependency:

dependencies:
  google_generative_ai: ^0.2.0
  flutter:
    sdk: flutter
  http: ^1.1.0
  flutter_dotenv: ^5.1.0  # For secure API key management

Run flutter pub get to install the dependencies. We're also including flutter_dotenv for secure API key management - never hardcode your API keys!

Setting Up Environment Variables

Create a .env file in your project root:

GEMINI_API_KEY=your_api_key_here

Don't forget to add .env to your .gitignore file to prevent accidentally committing your API key.

Basic Gemini Integration

Now let's create a simple service class to handle our Gemini AI interactions:

import 'package:google_generative_ai/google_generative_ai.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';

class GeminiService {
  late final GenerativeModel _model;
  
  GeminiService() {
    final apiKey = dotenv.env['GEMINI_API_KEY'];
    if (apiKey == null) {
      throw Exception('GEMINI_API_KEY not found in environment variables');
    }
    
    // Initialize the model
    _model = GenerativeModel(
      model: 'gemini-pro',
      apiKey: apiKey,
      generationConfig: GenerationConfig(
        temperature: 0.7,
        topK: 40,
        topP: 0.95,
        maxOutputTokens: 1024,
      ),
      safetySettings: [
        SafetySetting(HarmCategory.harassment, HarmBlockThreshold.mediumAndAbove),
        SafetySetting(HarmCategory.hateSpeech, HarmBlockThreshold.mediumAndAbove),
        SafetySetting(HarmCategory.sexuallyExplicit, HarmBlockThreshold.mediumAndAbove),
        SafetySetting(HarmCategory.dangerousContent, HarmBlockThreshold.mediumAndAbove),
      ],
    );
  }
  
  Future generateContent(String prompt) async {
    try {
      final content = [Content.text(prompt)];
      final response = await _model.generateContent(content);
      
      return response.text ?? 'No response generated';
    } catch (e) {
      throw Exception('Failed to generate content: $e');
    }
  }
}

Building a Chat Interface

Let's create a beautiful chat interface that leverages Gemini's conversational abilities. We'll build a stateful widget that manages the chat session:

import 'package:flutter/material.dart';
import 'package:google_generative_ai/google_generative_ai.dart';

class GeminiChatScreen extends StatefulWidget {
  const GeminiChatScreen({Key? key}) : super(key: key);
  
  @override
  State createState() => _GeminiChatScreenState();
}

class _GeminiChatScreenState extends State {
  final TextEditingController _controller = TextEditingController();
  final List _messages = [];
  late final ChatSession _chatSession;
  bool _isLoading = false;
  
  @override
  void initState() {
    super.initState();
    _initializeChat();
  }
  
  void _initializeChat() {
    final model = GenerativeModel(
      model: 'gemini-pro',
      apiKey: dotenv.env['GEMINI_API_KEY']!,
    );
    
    _chatSession = model.startChat();
    
    // Add welcome message
    setState(() {
      _messages.add(ChatMessage(
        text: 'Hello! I\'m Gemini AI. How can I help you today?',
        isUser: false,
        timestamp: DateTime.now(),
      ));
    });
  }
  
  Future _sendMessage() async {
    if (_controller.text.trim().isEmpty) return;
    
    final userMessage = _controller.text;
    _controller.clear();
    
    // Add user message to chat
    setState(() {
      _messages.add(ChatMessage(
        text: userMessage,
        isUser: true,
        timestamp: DateTime.now(),
      ));
      _isLoading = true;
    });
    
    try {
      final response = await _chatSession.sendMessage(
        Content.text(userMessage),
      );
      
      // Add AI response to chat
      setState(() {
        _messages.add(ChatMessage(
          text: response.text ?? 'No response',
          isUser: false,
          timestamp: DateTime.now(),
        ));
      });
    } catch (e) {
      // Handle error
      setState(() {
        _messages.add(ChatMessage(
          text: 'Error: ${e.toString()}',
          isUser: false,
          timestamp: DateTime.now(),
          isError: true,
        ));
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Gemini AI Chat'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              padding: const EdgeInsets.all(16),
              itemCount: _messages.length,
              itemBuilder: (context, index) {
                final message = _messages[index];
                return ChatBubble(message: message);
              },
            ),
          ),
          if (_isLoading)
            const LinearProgressIndicator(),
          _buildInputArea(),
        ],
      ),
    );
  }
  
  Widget _buildInputArea() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.grey[100],
        boxShadow: [
          BoxShadow(
            offset: const Offset(0, -2),
            blurRadius: 4,
            color: Colors.black.withOpacity(0.1),
          ),
        ],
      ),
      child: Row(
        children: [
          Expanded(
            child: TextField(
              controller: _controller,
              decoration: InputDecoration(
                hintText: 'Type your message...',
                filled: true,
                fillColor: Colors.white,
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(24),
                  borderSide: BorderSide.none,
                ),
                contentPadding: const EdgeInsets.symmetric(
                  horizontal: 20,
                  vertical: 12,
                ),
              ),
              onSubmitted: (_) => _sendMessage(),
            ),
          ),
          const SizedBox(width: 8),
          CircleAvatar(
            backgroundColor: Theme.of(context).primaryColor,
            child: IconButton(
              icon: const Icon(Icons.send, color: Colors.white),
              onPressed: _sendMessage,
            ),
          ),
        ],
      ),
    );
  }
}

Implementing Streaming Responses

One of Gemini's powerful features is the ability to stream responses, providing a more interactive experience. Let's modify our chat to support streaming:

Future _sendMessageWithStreaming() async {
  if (_controller.text.trim().isEmpty) return;
  
  final userMessage = _controller.text;
  _controller.clear();
  
  // Add user message
  setState(() {
    _messages.add(ChatMessage(
      text: userMessage,
      isUser: true,
      timestamp: DateTime.now(),
    ));
    _isLoading = true;
  });
  
  // Create a message for the AI response that we'll update
  final aiMessage = ChatMessage(
    text: '',
    isUser: false,
    timestamp: DateTime.now(),
  );
  
  setState(() {
    _messages.add(aiMessage);
  });
  
  try {
    final response = _chatSession.sendMessageStream(
      Content.text(userMessage),
    );
    
    String fullResponse = '';
    
    await for (final chunk in response) {
      fullResponse += chunk.text ?? '';
      
      // Update the AI message in real-time
      setState(() {
        aiMessage.text = fullResponse;
      });
    }
  } catch (e) {
    setState(() {
      aiMessage.text = 'Error: ${e.toString()}';
      aiMessage.isError = true;
    });
  } finally {
    setState(() {
      _isLoading = false;
    });
  }
}

Multimodal Capabilities

Gemini's multimodal capabilities allow it to understand images alongside text. Let's add image analysis to our chat:

import 'package:image_picker/image_picker.dart';
import 'dart:io';

Future _sendImageWithPrompt(File imageFile, String prompt) async {
  setState(() {
    _isLoading = true;
  });
  
  try {
    // Read image bytes
    final bytes = await imageFile.readAsBytes();
    
    // Create content with both image and text
    final content = [
      Content.multi([
        TextPart(prompt),
        DataPart('image/jpeg', bytes),
      ])
    ];
    
    // For image analysis, we need to use gemini-pro-vision
    final model = GenerativeModel(
      model: 'gemini-pro-vision',
      apiKey: dotenv.env['GEMINI_API_KEY']!,
    );
    
    final response = await model.generateContent(content);
    
    setState(() {
      _messages.add(ChatMessage(
        text: response.text ?? 'No response',
        isUser: false,
        timestamp: DateTime.now(),
        attachedImage: imageFile,
      ));
    });
  } catch (e) {
    print('Error analyzing image: $e');
  } finally {
    setState(() {
      _isLoading = false;
    });
  }
}

// Add image picker button to your UI
IconButton(
  icon: const Icon(Icons.image),
  onPressed: () async {
    final ImagePicker picker = ImagePicker();
    final XFile? image = await picker.pickImage(source: ImageSource.gallery);
    
    if (image != null) {
      final file = File(image.path);
      await _sendImageWithPrompt(file, 'What can you tell me about this image?');
    }
  },
)

Best Practices and Security

When building production-ready AI applications with Gemini, consider these best practices:

1. API Key Security

2. Error Handling

enum GeminiErrorType {
  networkError,
  quotaExceeded,
  invalidApiKey,
  contentFiltered,
  unknown
}

class GeminiErrorHandler {
  static GeminiErrorType identifyError(dynamic error) {
    if (error.toString().contains('quota')) {
      return GeminiErrorType.quotaExceeded;
    } else if (error.toString().contains('API key')) {
      return GeminiErrorType.invalidApiKey;
    } else if (error.toString().contains('blocked')) {
      return GeminiErrorType.contentFiltered;
    } else if (error.toString().contains('network')) {
      return GeminiErrorType.networkError;
    }
    return GeminiErrorType.unknown;
  }
  
  static String getErrorMessage(GeminiErrorType type) {
    switch (type) {
      case GeminiErrorType.quotaExceeded:
        return 'API quota exceeded. Please try again later.';
      case GeminiErrorType.invalidApiKey:
        return 'Invalid API key. Please check your configuration.';
      case GeminiErrorType.contentFiltered:
        return 'Content was filtered due to safety settings.';
      case GeminiErrorType.networkError:
        return 'Network error. Please check your connection.';
      default:
        return 'An unexpected error occurred. Please try again.';
    }
  }
}

3. Rate Limiting

Implement rate limiting to prevent API abuse and manage costs:

class RateLimiter {
  final int maxRequests;
  final Duration window;
  final List _timestamps = [];
  
  RateLimiter({
    this.maxRequests = 60,
    this.window = const Duration(minutes: 1),
  });
  
  bool canMakeRequest() {
    final now = DateTime.now();
    _timestamps.removeWhere(
      (timestamp) => now.difference(timestamp) > window,
    );
    
    if (_timestamps.length < maxRequests) {
      _timestamps.add(now);
      return true;
    }
    return false;
  }
  
  Duration timeUntilNextRequest() {
    if (_timestamps.isEmpty) return Duration.zero;
    
    final oldestTimestamp = _timestamps.first;
    final timePassed = DateTime.now().difference(oldestTimestamp);
    
    if (timePassed > window) return Duration.zero;
    
    return window - timePassed;
  }
}

4. Content Caching

Cache responses to improve performance and reduce API calls:

import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

class GeminiCache {
  static const String _cacheKey = 'gemini_cache';
  static const Duration _cacheExpiry = Duration(hours: 24);
  
  static Future getCachedResponse(String prompt) async {
    final prefs = await SharedPreferences.getInstance();
    final cacheData = prefs.getString(_cacheKey);
    
    if (cacheData == null) return null;
    
    final cache = json.decode(cacheData) as Map;
    final entry = cache[prompt];
    
    if (entry == null) return null;
    
    final timestamp = DateTime.parse(entry['timestamp']);
    if (DateTime.now().difference(timestamp) > _cacheExpiry) {
      return null;
    }
    
    return entry['response'];
  }
  
  static Future cacheResponse(String prompt, String response) async {
    final prefs = await SharedPreferences.getInstance();
    final cacheData = prefs.getString(_cacheKey);
    
    final cache = cacheData != null
        ? json.decode(cacheData) as Map
        : {};
    
    cache[prompt] = {
      'response': response,
      'timestamp': DateTime.now().toIso8601String(),
    };
    
    await prefs.setString(_cacheKey, json.encode(cache));
  }
}

Advanced Features

Function Calling

Gemini supports function calling, allowing the AI to interact with your app's functionality:

final functions = [
  FunctionDeclaration(
    'getCurrentWeather',
    'Get the current weather for a location',
    Schema(
      SchemaType.object,
      properties: {
        'location': Schema(SchemaType.string),
        'unit': Schema(
          SchemaType.string,
          enumValues: ['celsius', 'fahrenheit'],
        ),
      },
      requiredProperties: ['location'],
    ),
  ),
];

final model = GenerativeModel(
  model: 'gemini-pro',
  apiKey: apiKey,
  tools: [Tool(functionDeclarations: functions)],
);

// Handle function calls in responses
final response = await chat.sendMessage(content);
final functionCall = response.functionCalls.firstOrNull;

if (functionCall != null) {
  final result = await executeFunction(functionCall);
  // Send function result back to continue conversation
  await chat.sendMessage(Content.functionResponse(
    functionCall.name,
    result,
  ));
}

Custom System Instructions

Set up system instructions to customize Gemini's behavior:

final chat = model.startChat(
  history: [
    Content.text('''You are a helpful Flutter development assistant. 
    You specialize in:
    - Flutter best practices
    - Dart programming
    - Mobile app architecture
    - Performance optimization
    
    Always provide code examples when relevant and explain your reasoning.
    Format code blocks with proper syntax highlighting.'''),
    Content.model([TextPart('Understood! I\'ll help you with Flutter development.')]),
  ],
);

Conclusion and Next Steps

Congratulations! You've learned how to integrate Google's Gemini AI into your Flutter applications. We've covered everything from basic setup to advanced features like streaming responses and multimodal capabilities. Gemini's powerful AI capabilities combined with Flutter's cross-platform framework create endless possibilities for intelligent mobile applications.

Key Takeaways

Next Steps

  1. Experiment with Different Models: Try gemini-pro-vision for image understanding
  2. Build a Complete App: Create a full-featured AI assistant with persistent chat history
  3. Optimize Performance: Implement caching and efficient state management
  4. Add Voice Integration: Combine Gemini with speech-to-text for voice interactions
  5. Deploy to Production: Set up proper backend infrastructure for API key management

About the Author

Utsav Savani is the founder of Innovative Flutter AI and a full-stack product engineer with 8+ years of experience. He designs in Figma, builds in Flutter and ships the backend on Supabase, Firebase and AWS, and leads a 50-person team. Find him on GitHub and LinkedIn.

Work with us

Building something like this?

We design, build and ship Flutter apps with AI features for startups and product teams. Tell us the idea and we will tell you what it takes.

Start a project More articles

Thanks, it's sent. We'll reply within one business day. Close