「スマホアプリにAIを組み込みたいけど、どこから手をつければいいかわからない…」

そんな悩みを抱えているFlutter開発者の方、多いんじゃないでしょうか。

今回は、FlutterアプリにClaude APIを組み込んで、スマホで動くAIチャット画面をゼロから作る方法を解説します。バックエンドにPythonのFastAPIを使う構成なので、セキュリティ面もしっかり押さえられますよ。

難しそうに見えて、実は構成はシンプルです。「むずかしそう」を「できそう」に変えていきましょう!

🎯 この記事の対象読者と完成イメージ

mobile chat app AI
mobile chat app AI / Photo by Patrick Gamelkoorn via Pexels

対象読者: Flutter基礎がわかる初〜中級者、Claude APIを触ったことがある方

完成するのは、こんなアプリです。

  • ✅ スマホ画面でテキストを入力してAIに送信
  • ✅ Claude APIが返答を返してチャット形式で表示
  • ✅ PythonバックエンドがAPIキーを安全に管理

アーキテクチャをざっくり図解するとこんな感じです。

Flutterアプリ(UI) → Python FastAPI(バックエンド) → Claude API

FlutterアプリがAPIキーを直接持たないのがポイントです。APIキーをアプリに埋め込むのはセキュリティ上NGなので、バックエンド経由でClaude APIを叩く構成にします。

🐍 STEP1:PythonバックエンドをFastAPIで作る

まずはFlutterから受け取ったメッセージをClaude APIに渡すバックエンドを作りましょう。

必要なライブラリをインストールします。

pip install fastapi uvicorn anthropic python-dotenv

次に、.env ファイルにAPIキーを保存します。

ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxx

そして、main.py を作成します。

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import anthropic
import os
from dotenv import load_dotenv

load_dotenv()

app = FastAPI()

# Flutter(モバイル)からのリクエストを許可する
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 本番環境では特定のIPに制限してください
    allow_methods=["*"],
    allow_headers=["*"],
)

# リクエストのデータ型を定義
class ChatRequest(BaseModel):
    message: str
    history: list[dict] = []  # 会話履歴(オプション)

client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

@app.post("/chat")
async def chat(request: ChatRequest):
    # 会話履歴にユーザーの新しいメッセージを追加
    messages = request.history + [
        {"role": "user", "content": request.message}
    ]

    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=1024,
        messages=messages
    )

    # Claudeの返答テキストを取り出して返す
    reply = response.content[0].text
    return {"reply": reply}

ポイントをまとめるとこんな感じです。

  • CORSMiddleware:FlutterアプリはスマホのHTTPクライアントからリクエストを送るため、CORS設定が必要
  • history フィールド:過去の会話履歴を受け取ることで、文脈を持った返答が可能になる
  • APIキーは .env で管理:コードに直書きしないのが鉄則

起動はこちらのコマンドで。

uvicorn main:app --host 0.0.0.0 --port 8000 --reload

http://localhost:8000/docs にアクセスして、Swagger UIでAPIが動くか確認しておきましょう。

📱 STEP2:FlutterのチャットUIを作る

次は Flutter 側です。シンプルなチャット画面を作っていきます。

まず pubspec.yamlhttp パッケージを追加します。

dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.1

追加したら flutter pub get を忘れずに。

次に lib/main.dart を以下のように作成します。

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'AI Chat',
      theme: ThemeData(colorSchemeSeed: Colors.indigo),
      home: const ChatScreen(),
    );
  }
}

class ChatMessage {
  final String text;
  final bool isUser; // trueならユーザー、falseならAI
  ChatMessage({required this.text, required this.isUser});
}

class ChatScreen extends StatefulWidget {
  const ChatScreen({super.key});
  @override
  State<ChatScreen> createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  final List<ChatMessage> _messages = [];
  final List _history = []; // Claude APIに渡す履歴
  final TextEditingController _controller = TextEditingController();
  bool _isLoading = false;

  // バックエンドのURL(実機テストの場合はPCのローカルIPに変更)
  final String _apiUrl = 'http://10.0.2.2:8000/chat'; // Android Emulatorの場合

  Future<void> _sendMessage() async {
    final text = _controller.text.trim();
    if (text.isEmpty) return;

    setState(() {
      _messages.add(ChatMessage(text: text, isUser: true));
      _isLoading = true;
    });
    _controller.clear();

    try {
      final response = await http.post(
        Uri.parse(_apiUrl),
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode({
          'message': text,
          'history': _history,
        }),
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        final reply = data['reply'] as String;

        // 会話履歴を更新(次回以降の文脈のため)
        _history.add({'role': 'user', 'content': text});
        _history.add({'role': 'assistant', 'content': reply});

        setState(() {
          _messages.add(ChatMessage(text: reply, isUser: false));
        });
      }
    } catch (e) {
      setState(() {
        _messages.add(ChatMessage(text: 'エラーが発生しました: $e', isUser: false));
      });
    } finally {
      setState(() => _isLoading = false);
    }
  }

  Widget _buildBubble(ChatMessage msg) {
    final isUser = msg.isUser;
    return Align(
      alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
        padding: const EdgeInsets.all(12),
        constraints: const BoxConstraints(maxWidth: 280),
        decoration: BoxDecoration(
          color: isUser ? Colors.indigo : Colors.grey[200],
          borderRadius: BorderRadius.circular(16),
        ),
        child: Text(
          msg.text,
          style: TextStyle(color: isUser ? Colors.white : Colors.black87),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AI Chat(Claude)')),
      body: Column(
        children: [
          // メッセージ一覧
          Expanded(
            child: ListView.builder(
              padding: const EdgeInsets.symmetric(vertical: 8),
              itemCount: _messages.length,
              itemBuilder: (_, i) => _buildBubble(_messages[i]),
            ),
          ),
          // ローディング表示
          if (_isLoading)
            const Padding(
              padding: EdgeInsets.all(8),
              child: CircularProgressIndicator(),
            ),
          // 入力エリア
          Padding(
            padding: const EdgeInsets.all(8),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'メッセージを入力...',
                      border: OutlineInputBorder(),
                    ),
                    onSubmitted: (_) => _sendMessage(),
                  ),
                ),
                const SizedBox(width: 8),
                IconButton(
                  onPressed: _isLoading ? null : _sendMessage,
                  icon: const Icon(Icons.send),
                  color: Colors.indigo,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

ここが重要です。URLの設定について確認しておきましょう。

  • Android エミュレーターhttp://10.0.2.2:8000(PC のlocalhostへのエイリアス)
  • iOS シミュレーターhttp://localhost:8000
  • 実機テスト:PCのローカルIPアドレス(例:http://192.168.1.xxx:8000

実機でテストする場合は、スマホとPCが同じWi-Fiに接続されている必要があります。

🔒 STEP3:本番環境でのセキュリティ対策


ローカルでの動作確認ができたら、本番を意識した対策も確認しておきましょう。

① CORS を特定のオリジンに制限する

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://yourdomain.com"],  # 特定ドメインのみ許可
    allow_methods=["POST"],
    allow_headers=["Content-Type"],
)

② レートリミットで過剰リクエストを防ぐ

Claude APIは従量課金なので、悪意あるリクエストを放置するとコストが爆発します。slowapi などのライブラリでレートリミットをかけておくのがおすすめです。

③ バックエンドをクラウドにデプロイする

本番運用では、PythonバックエンドをRender・Railway・Fly.ioなどにデプロイします。環境変数(ANTHROPIC_API_KEY)はクラウドサービスの設定画面から登録しましょう。

🚀 動作確認のポイント


実装が終わったら、以下の順番で動作確認してみてください。

  1. Pythonバックエンドを起動(uvicorn main:app --reload
  2. Swagger UI(http://localhost:8000/docs)でAPIに直接リクエストを送ってみる
  3. FlutterをAndroidエミュレーター or 実機で起動(flutter run
  4. チャット画面でメッセージを送信し、AIの返答が表示されることを確認

Swagger UIで先にAPIの動作を確認しておくと、FlutterとPythonどちらに問題があるかを切り分けやすくなります。詰まったときに便利ですよ。

まとめ

今回はFlutter × Claude API × Python FastAPIで、スマホで動くAIチャットアプリをゼロから実装する方法を解説しました。

ポイントをおさらいするとこんな感じです。

  • 🔑 APIキーはPythonバックエンドで管理し、Flutterアプリには持たせない
  • 🗂️ 会話履歴を history で管理することで、文脈のある会話が実現できる
  • 📡 エミュレーターと実機でURLの設定が異なるので要注意

この構成を土台にすれば、UI の改善やストリーミング返答への対応など、さらに発展させるのも難しくありません。ぜひ手元で動かしてみてください!🎉

📚 関連商品・おすすめ書籍

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

もしも

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

初心者に定番のPython入門書

Amazonで見る

実践Claude Code入門―現場で活用するためのAIコーディングの思考法

もしも

実践Claude Code入門―現場で活用するためのAIコーディングの思考法

AIコーディングの現場活用法を学ぶ一冊

Amazonで見る

Python Web開発実践入門 ―― FastAPIによるWebAPI開発と非同期処理

もしも

Python Web開発実践入門 ―― FastAPIによるWebAPI開発と非同期処理

FastAPIでWebAPI開発を実践的に学ぶ

Amazonで見る

※本記事にはアフィリエイトリンクが含まれます。

ABOUT ME
やまちゃん
これまで学生と社会人を合わせて5000人以上にプログラミング学習を指導。 ゼロからイチをわかりやすく解説する専門家として活動しており、本業ではArduinoを用いたIoT開発とロボットプログラミングが専門。 Pythonを用いたアプリ開発、ウェブアプリケーションの開発で業務の効率化をサポートしています。