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

Flutter AI Chat App | GPT, Claude, Gemini & Grok in One App | Part 1
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

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

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

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