Site icon AndroidRide

Flutter AI Chat App: ChatGpt, Claude, Gemini and Grok In One Flutter App

Do you search the same prompt across different AI models?

I do it too.

So I thought, why not build a simple Flutter app that lets you chat with multiple AI models from one place?

In this tutorial, we’ll integrate GPT, Claude, Gemini, and Grok into a single Flutter app.

Let’s start building it.

flutter ai chat app

https://youtu.be/yzkcDEvCok4
okay enough talk…Let’s start build

Okay… Let’s make a simple Flutter project named **`multi_chat`** using the `flutter create` command.

If you don’t know how to make a Flutter project read this command guide and Flutter Android Studio setup guide.

lets add the packages in pubspec.yaml file and also setup .env setup declaration.


name: multi_chat
description: "A new Flutter project."
publish_to: 'none' 

version: 1.0.0+1

environment:
  sdk: ^3.12.2

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.8
  http: ^1.6.0
  flutter_spinkit: ^5.2.2
  flutter_markdown_plus: ^1.0.12
  url_launcher: ^6.3.0
  flutter_dotenv: ^6.0.1

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^6.0.0

flutter:

  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  assets:
    - .env



main.dart
we will setup UI in main.dart file.


import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:multi_chat/models/llm_provider.dart';
import 'package:multi_chat/services/llm_service.dart';
import 'package:url_launcher/url_launcher.dart';

Future main() async {
  await dotenv.load();
  runApp(
    const MaterialApp(
      title: 'MultiChat',
      debugShowCheckedModeBanner: false,
      home: MultiChatScreen(),
    ),
  );
}

class MultiChatScreen extends StatefulWidget {
  const MultiChatScreen({super.key});

  @override
  State createState() => _MultiChatScreenState();
}

class _MultiChatScreenState extends State {
  final TextEditingController _searchController = TextEditingController();
  bool loading = false;
  String _response = '';

  LLMProvider _selectedProvider = LLMProvider.gemini;

  String _providerTitle(LLMProvider provider) => switch (provider) {
    LLMProvider.gemini => 'Gemini',
    LLMProvider.claude => 'Claude',
    LLMProvider.gpt => 'GPT',
    LLMProvider.grok => 'Grok',
  };

  String _providerSubTitle(LLMProvider provider) => switch (provider) {
    LLMProvider.gemini => "Google's AI Model",
    LLMProvider.claude => "Anthropic's AI Model",
    LLMProvider.gpt => "OpenAI's AI Model",
    LLMProvider.grok => "xAI's AI Model",
  };

  IconData _providerIcon(LLMProvider provider) => switch (provider) {
    LLMProvider.gemini => Icons.auto_awesome,
    LLMProvider.claude => Icons.psychology_outlined,
    LLMProvider.gpt => Icons.smart_toy_outlined,
    LLMProvider.grok => Icons.bolt_outlined,
  };

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    return SafeArea(
      child: Scaffold(
        body: Column(
          children: [
            Expanded(
              child: _response.isEmpty
                  ? Center(
                      child: Column(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          Text(
                            'ANDROIDRIDE',
                            style: TextStyle(
                              fontWeight: FontWeight.bold,
                              fontSize: 35,
                            ),
                          ),
                          if (loading)
                            Column(
                              children: [
                                SizedBox(height: 8),
                                SpinKitThreeBounce(
                                  color: colorScheme.onSurface,
                                  size: 20,
                                ),
                              ],
                            ),
                        ],
                      ),
                    )
                  : SingleChildScrollView(
                      padding: const EdgeInsets.all(16),
                      child: MarkdownBody(
                        data: _response,
                        styleSheet: MarkdownStyleSheet(
                          p: TextStyle(
                            color: colorScheme.onSurface,
                            fontSize: 16,
                          ),
                          h1: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                        ),
                        onTapLink: (text, href, title) {
                          if (href != null) {
                            launchUrl(Uri.parse(href));
                          }
                        },
                      ),
                    ),
            ),
            Container(
              padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
              decoration: BoxDecoration(
                color: colorScheme.surface,
                border: Border(
                  top: BorderSide(color: colorScheme.outlineVariant),
                ),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  ActionChip(
                    label: Text(_providerTitle(_selectedProvider)),
                    avatar: Icon(_providerIcon(_selectedProvider), size: 18),
                    onPressed: loading ? null : showProviderBottomSheet,
                  ),
                  const SizedBox(height: 8),
                  Material(
                    elevation: 1,
                    borderRadius: BorderRadius.circular(24),
                    color: colorScheme.surfaceContainerHighest,
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.end,
                      children: [
                        Expanded(
                          child: TextField(
                            controller: _searchController,
                            minLines: 1,
                            maxLines: 4,
                            enabled: !loading,
                            textInputAction: TextInputAction.send,
                            onSubmitted: (_) => _ask(),
                            decoration: InputDecoration(
                              hintText: 'Ask something...',
                              border: InputBorder.none,
                              contentPadding: EdgeInsets.symmetric(
                                horizontal: 16,
                                vertical: 8,
                              ),
                            ),
                          ),
                        ),
                        Padding(
                          padding: const EdgeInsets.all(4.0),
                          child: IconButton.filled(
                            onPressed: loading ? null : _ask,
                            icon: const Icon(Icons.arrow_upward),
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  void showProviderBottomSheet() {
    showModalBottomSheet(
      context: context,
      showDragHandle: true,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
      ),
      builder: (context) {
        return SafeArea(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Padding(
                padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
                child: Align(
                  alignment: Alignment.centerLeft,
                  child: Text(
                    "Choose Provider",
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                ),
              ),
              ...LLMProvider.values.map((provider) {
                final isSelected = provider == _selectedProvider;
                return ListTile(
                  leading: CircleAvatar(
                    child: Icon(
                      _providerIcon(provider),
                      size: 18,
                      color: Theme.of(context).colorScheme.onPrimaryContainer,
                    ),
                  ),
                  title: Text(_providerTitle(provider)),
                  subtitle: Text(_providerSubTitle(provider)),
                  trailing: isSelected
                      ? Icon(
                          Icons.check_circle,
                          color: Theme.of(context).colorScheme.primary,
                        )
                      : null,
                  onTap: () {
                    setState(() {
                      _selectedProvider = provider;
                      Navigator.pop(context);
                    });
                  },
                );
              }),
            ],
          ),
        );
      },
    );
  }

  void _ask() async {
    final prompt = _searchController.text.trim();

    if (prompt.isEmpty || loading) return;

    FocusManager.instance.primaryFocus?.unfocus();

    setState(() {
      loading = true;
      _response = '';
    });

    final response = switch (_selectedProvider) {
      LLMProvider.gemini => await LlmService.instance.generageGeminiContent(
        prompt,
      ),
      LLMProvider.claude => await LlmService.instance.generageClaudeContent(
        prompt,
      ),
      LLMProvider.gpt => await LlmService.instance.generageGPTContent(prompt),
      LLMProvider.grok => await LlmService.instance.generageGrokContent(prompt),
    };


    setState(() {
      loading = false;
      _response = response;
    });
  }
}


.env file
Please put your api keys here. If not, you can directly use your api keys in your code, but thats a bad practice.


GEMINI_API_KEY =“YOUR_API_KEY”
CLAUDE_API_KEY =“YOUR_API_KEY”
GPT_API_KEY =“YOUR_API_KEY”
GROK_API_KEY =“YOUR_API_KEY”

llm_provider.dart


enum LLMProvider { gemini, claude, gpt, grok }

Using an enum makes code easier to maintain, If more llms are added in future.

Before API Integration

Gemini API Integration

Flutter AI Chat App - Gemini API Integration |  Part 3

Gemini API Key URL


  static const _geminiUrl =
      "https://generativelanguage.googleapis.com/v1/interactions";

 Future generageGeminiContent(String prompt) async {
    final geminiApiKey = dotenv.get('GEMINI_API_KEY');

    try {
      final response = await http.post(
        Uri.parse(_geminiUrl),
        headers: {
          'x-goog-api-key': geminiApiKey,
          'Content-Type': 'application/json',
        },
        body: jsonEncode({"model": "gemini-2.5-flash", "input": prompt}),
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        return data['steps'][1]["content"][0]["text"];
      } else {
        return 'Error: ${response.statusCode} - ${response.body}';
      }
    } catch (e) {
      return 'Error: $e';
    }
  }


Use postman to API are working with needed headers and other fields.
gemini ai postman integration

Claude API Integration

Flutter AI Chat App – CLAUDE API INTEGRATION | Part 5
Claude API Key URL
claude api integration in postman


  Future generageClaudeContent(String prompt) async {
    final claudeApiKey = dotenv.get('CLAUDE_API_KEY');

    try {
      final response = await http.post(
        Uri.parse(_claudeUrl),
        headers: {
          'x-api-key': claudeApiKey,
          'Content-Type': 'application/json',
          'anthropic-version': '2023-06-01',
        },
        body: jsonEncode({
          "model": "claude-haiku-4-5-20251001",
          "max_tokens": 10000,
          "messages": [
            {"role": "user", "content": prompt},
          ],
        }),
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        return data['content'][0]["text"];
      } else {
        return 'Error: ${response.statusCode} - ${response.body}';
      }
    } catch (e) {
      return 'Error: $e';
    }
  }

GPT API Integration

Flutter AI Chat App – OPENAI API INTEGRATION | Part 6
GPT API Key URL
gpt api integration in postman


  static const _openAiUrl = "https://api.openai.com/v1/responses";

Future generageGPTContent(String prompt) async {
    final gptApiKey = dotenv.get('GPT_API_KEY');

    try {
      final response = await http.post(
        Uri.parse(_openAiUrl),
        headers: {
          'Authorization': 'Bearer $gptApiKey',
          'Content-Type': 'application/json',
        },
        body: jsonEncode({"model": "gpt-5.4", "input": prompt}),
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        return data['output'][0]["content"][0]["text"];
      } else {
        return 'Error: ${response.statusCode} - ${response.body}';
      }
    } catch (e) {
      return 'Error: $e';
    }
  }

Grok API Integration

GROK API Key URL
grok api integration

  static const _grokUrl = "https://api.x.ai/v1/responses";

  Future generageGrokContent(String prompt) async {
    final grokApiKey = dotenv.get('GROK_API_KEY');

    try {
      final response = await http.post(
        Uri.parse(_grokUrl),
        headers: {
          'Authorization': 'Bearer $grokApiKey',
          'Content-Type': 'application/json',
        },
        body: jsonEncode(
          {
           "model": "grok-4.5", 
          "input": prompt
          }),
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        return data['output'][1]["content"][0]["text"];
      } else {
        return 'Error: ${response.statusCode} - ${response.body}';
      }
    } catch (e) {
      return 'Error: $e';
    }
  }


llm_service.dart

All LLM API integration logic is handled in this file, making it easy to integrate additional LLMs in the future.


import 'dart:convert';

import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;

class LlmService {
  LlmService._();

  static final LlmService instance = LlmService._();

  static const _geminiUrl =
      "https://generativelanguage.googleapis.com/v1/interactions";

  static const _claudeUrl = "https://api.anthropic.com/v1/messages";

  static const _openAiUrl = "https://api.openai.com/v1/responses";

  static const _grokUrl = "https://api.x.ai/v1/responses";

  Future generageGrokContent(String prompt) async {
    final grokApiKey = dotenv.get('GROK_API_KEY');

    try {
      final response = await http.post(
        Uri.parse(_grokUrl),
        headers: {
          'Authorization': 'Bearer $grokApiKey',
          'Content-Type': 'application/json',
        },
        body: jsonEncode(
          {
           "model": "grok-4.5", 
          "input": prompt
          }),
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        return data['output'][1]["content"][0]["text"];
      } else {
        return 'Error: ${response.statusCode} - ${response.body}';
      }
    } catch (e) {
      return 'Error: $e';
    }
  }

  Future generageGPTContent(String prompt) async {
    final gptApiKey = dotenv.get('GPT_API_KEY');

    try {
      final response = await http.post(
        Uri.parse(_openAiUrl),
        headers: {
          'Authorization': 'Bearer $gptApiKey',
          'Content-Type': 'application/json',
        },
        body: jsonEncode({"model": "gpt-5.4", "input": prompt}),
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        return data['output'][0]["content"][0]["text"];
      } else {
        return 'Error: ${response.statusCode} - ${response.body}';
      }
    } catch (e) {
      return 'Error: $e';
    }
  }

  Future generageGeminiContent(String prompt) async {
    final geminiApiKey = dotenv.get('GEMINI_API_KEY');

    try {
      final response = await http.post(
        Uri.parse(_geminiUrl),
        headers: {
          'x-goog-api-key': geminiApiKey,
          'Content-Type': 'application/json',
        },
        body: jsonEncode({"model": "gemini-2.5-flash", "input": prompt}),
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        return data['steps'][1]["content"][0]["text"];
      } else {
        return 'Error: ${response.statusCode} - ${response.body}';
      }
    } catch (e) {
      return 'Error: $e';
    }
  }

  Future generageClaudeContent(String prompt) async {
    final claudeApiKey = dotenv.get('CLAUDE_API_KEY');

    try {
      final response = await http.post(
        Uri.parse(_claudeUrl),
        headers: {
          'x-api-key': claudeApiKey,
          'Content-Type': 'application/json',
          'anthropic-version': '2023-06-01',
        },
        body: jsonEncode({
          "model": "claude-haiku-4-5-20251001",
          "max_tokens": 10000,
          "messages": [
            {"role": "user", "content": prompt},
          ],
        }),
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        return data['content'][0]["text"];
      } else {
        return 'Error: ${response.statusCode} - ${response.body}';
      }
    } catch (e) {
      return 'Error: $e';
    }
  }
}


Thats all for now.

Exit mobile version