【Flutter】Gemini APIの使い方|AIに質問するアプリを作成

当サイトで紹介する商品・サービス等の外部リンクは、アフィリエイト広告を含む場合があります。
スポンサーリンク
本記事を読むと以下の実行ができます

入力欄に書いた質問(マイクで話した質問も)を Gemini に送り、AI の返事を画面に表示するアプリを作れます。

アプリに AI を組み込みたいとき、手軽に試せるのが Google の Gemini API です。文章を送ると、AI が考えた返事が文章で返ってきます。

Flutter からは、パッケージを1つ追加するだけで Gemini を呼び出せます。ただし、混雑して返事が来ないことがある、返事に記号がそのまま出るなど、動かしてみて初めて気づく落とし穴もあります。

本記事では、質問を送って返事を表示するまでの流れを解説します。また、混雑して返事が来ないときに、自動でやり直す仕組みも入れます。使っているパッケージが非推奨になった点と、これから始める人向けの選び方も紹介します。

本記事は次の人におすすめ
  • Flutter のアプリから Gemini(AI)を呼び出したい人
  • Gemini API に質問を送って、返事を画面に出すまでの流れを知りたい人
  • 返事が来ない(混雑する)ときの対策を知りたい人
  • APIキーの扱いで気をつけることを知りたい人
スポンサーリンク

Gemini API(google_generative_ai)

Gemini API はGoogle の AI「Gemini」を使うための窓口で、質問の文章を送るとGemini が考えた返事が文章で返ってきます。

そのため、Flitterに入れることでAIを用いたアプリが作成可能になります。

Flutter からは、google_generative_ai というパッケージで呼び出せます。本記事のコードは、このパッケージの 0.4.7 で作っています。

筆者
筆者

Gemini を選んだのは、無料範囲があるからです。ChatGPT や Claude の API は、試すだけでも使った分の料金がかかるため、まずは Geminiで連携を作りました。

一方で、Geminiも上限を超えると従量課金になり、モデルによってChatGPTの方が安くなることがあります。

APIキーの取得

Gemini API を使うには APIキー(アプリが自分のものだと Google に伝えるための合言葉)が必要です。

  1. Google AI Studio を開き、左下の鍵のアイコン(Get API key)を押す
  2. 「API キー」のページで、右上の API キーを作成 を押す
  3. 作ったキーをコピーしておく

作ったキーは、この一覧に並びます。「請求階層」が無料枠になっていれば、料金はかからない範囲で使えます。

筆者
筆者

無料枠でも、基本的なテストで使う程度なら、制限に当たることはほとんどありません。安心してFlutterと連携してください。

APIキーはパスワードと同じように扱います。GitHub などに上げないようにしてください。

参考:Using Gemini API keys

パッケージの追加と.envの設定

pubspec.yaml に2つのパッケージを追加します。google_generative_ai が Gemini を呼ぶパッケージ、flutter_dotenv が APIキーを別ファイルから読むパッケージです。

dependencies:
  google_generative_ai: ^0.4.7
  flutter_dotenv: ^6.0.1

flutter:
  assets:
    - .env

プロジェクトの直下(pubspec.yaml と同じ場所)に .env というファイルを作り、your_api_key_here の部分を、コピーしたキーに書き換えます。

GEMINI_API_KEY=api_key

アプリの起動時に、この .env を読み込みます。main.dart の main の最初に、次の2行を入れます。

WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: '.env');

キーをコードに直接書かず .env に分けておくと、コードだけを人に見せたり公開したりしても、キーが一緒に出ていきません。.env は .gitignore に入れておきましょう。ただし、Web で動かすときには別の注意があります(「エラーと解決策・対策」で説明します)。

参考:google_generative_ai

google_generative_aiは非推奨|これから始めるならFirebase AI Logic

google_generative_ai は、公式のパッケージページで非推奨(Deprecated)と表示されています。今後、機能の追加や変更はされない予定です。

本記事のコードは、まず手元で Gemini を試すために google_generative_ai で作りました。いまも動きますが、これから作り始めるアプリ、特に人に公開するアプリでは、後継の Firebase AI Logic(firebase_ai パッケージ)を使うのがおすすめです。

項目google_generative_aifirebase_ai(Firebase AI Logic)
状態非推奨。今後は変更されない開発が続いている
準備APIキーだけFirebase のプロジェクトが必要
アプリを守る仕組みなしApp Check(正規のアプリからの呼び出しかを確かめる仕組み)を強く推奨
呼び出し方GenerativeModel(…) を作り、generateContentFirebaseAI.googleAI().generativeModel(…) を作り、generateContent

呼び出し方はよく似ていて、モデルを作って generateContent に質問を渡す流れは同じです。本記事の解説は、そのまま firebase_ai でも役に立ちます。

参考:Get started with the Gemini API using the Firebase AI Logic SDKs

gemini-3.6-flashとflash-liteの使い分け

Gemini には、性能と速さの違うモデル(AI の種類)がいくつかあります。どのモデルを使うかは、コードの中でモデル名の文字列を書いて選びます。

モデル名特徴本記事での使い方
gemini-3.6-flash速さと性能のバランスがよい普段はこちらで返事を作る
gemini-3.5-flash-lite一番速く、費用も安い混雑して flash が返事をしないときの予備

本記事では、普段は flash を使い、混雑しているときだけ flash-lite に切り替えます。返事の質を保ちつつ、返事が来ないまま待たされることを減らすためです。

モデルは新しいものが次々に出ます。公式のモデル一覧には、gemini-3.6-flash より新しい世代の flash も載っています。モデル名は文字列1か所なので、新しいモデルに替えるときはそこを書き換えるだけです。

参考:Gemini models

Gemini APIに質問し返答する

ここまでの内容をもとに、次の動きをする画面を作ります。

  • 入力欄に質問を書く(マイクで話して入れてもよい)
  • 「Geminiに聞く」を押すと、質問を Gemini に送る
  • 返事を待つ間は、ボタンにくるくるを出して押せなくする
  • 返事が届いたら、入力欄の下に表示する(長い返事はスクロールできる)
  • 混雑していたら、少し待ってやり直し、それでもだめなら予備のモデルで返事を作る

マイクで話した内容を入力欄に入れる部分は、speech_to_text で作っています。

speech_to_text で音声入力を作る方法を解説しています。

ソースコード

  • gemini_service.dart

Gemini とのやり取りをまとめたクラスです。lib/services/ に置きます。

class GeminiService {
  GeminiService() {
    // .env から APIキーを読む
    final apiKey = dotenv.env['GEMINI_API_KEY'];
    if (apiKey != null && apiKey.isNotEmpty && apiKey != 'your_api_key_here') {
      _model = GenerativeModel(model: 'gemini-3.6-flash', apiKey: apiKey);
      // 混雑しているときに使う予備のモデル
      _fallbackModel = GenerativeModel(model: 'gemini-3.5-flash-lite', apiKey: apiKey);
    }
  }

  GenerativeModel? _model;
  GenerativeModel? _fallbackModel;

  bool get isConfigured => _model != null;

  // 質問を送り、返事の文章を返す
  Future<String> ask(String prompt) async {
    final model = _model;
    final fallbackModel = _fallbackModel;
    if (model == null || fallbackModel == null) {
      throw StateError('GEMINI_API_KEY が未設定です。.env に自分のAPIキーを設定してください。');
    }

    final content = [Content.text(prompt)];
    try {
      return _textOf(await _withRetryOnOverload(() => model.generateContent(content)));
    } on GenerativeAIException catch (e) {
      if (!_isOverloaded(e)) rethrow;
      // やり直しても混雑していたら、予備のモデルで返事を作る
      return _textOf(await fallbackModel.generateContent(content));
    }
  }

  // 混雑していたら2秒待って、1回だけやり直す
  Future<GenerateContentResponse> _withRetryOnOverload(
    Future<GenerateContentResponse> Function() call,
  ) async {
    try {
      return await call();
    } on GenerativeAIException catch (e) {
      if (!_isOverloaded(e)) rethrow;
      await Future<void>.delayed(const Duration(seconds: 2));
      return call();
    }
  }

  // エラーの文に 503 や UNAVAILABLE があれば「混雑」とみなす
  static bool _isOverloaded(GenerativeAIException e) =>
      e.message.contains('503') || e.message.contains('UNAVAILABLE');

  static String _textOf(GenerateContentResponse response) =>
      response.text ?? '(応答がありませんでした)';
}
  • speech_input_page.dart

画面の State に持たせる部分です。「Geminiに聞く」を押すと _askGemini が呼ばれます。

final GeminiService _gemini = GeminiService();
bool _isAsking = false; // 返事を待っている間は true
String _answer = ''; // Gemini の返事

// 入力欄の文章を Gemini に送り、返事を表示する
Future<void> _askGemini() async {
  final prompt = _textController.text.trim();
  if (prompt.isEmpty) return;

  // 先に _isAsking を true にしてボタンを押せなくする(返事待ちの間の二重送信を防ぐ)
  setState(() {
    _isAsking = true;
    _statusMessage = '';
    _isError = false;
    _answer = '';
  });

  // 聞き取り中なら、マイクを止めてから送る
  if (_isListening) {
    await _speech.stop();
    if (!mounted) return;
    setState(() => _isListening = false);
  }

  try {
    final answer = await _gemini.ask(prompt);
    if (!mounted) return; // 待っている間に画面が閉じられていたら何もしない
    setState(() => _answer = answer);
  } catch (e) {
    if (!mounted) return;
    setState(() {
      _statusMessage = 'Gemini呼び出しエラー: $e';
      _isError = true;
    });
  } finally {
    // 成功しても失敗しても、最後に必ずボタンを押せる状態に戻す
    if (mounted) setState(() => _isAsking = false);
  }
}

build の中に並べる部品です。送信ボタンと、返事の表示です。

// Gemini に送るボタン(返事待ちの間は押せず、くるくるを出す)
FilledButton.icon(
  onPressed: _isAsking ? null : _askGemini,
  icon: _isAsking
      ? const SizedBox(
          width: 16,
          height: 16,
          child: CircularProgressIndicator(strokeWidth: 2),
        )
      : const Icon(Icons.send),
  label: const Text('Geminiに聞く'),
),

// Gemini の返事(長い返事はスクロールできる)
if (_answer.isNotEmpty)
  Expanded(
    child: SingleChildScrollView(
      // 右下のマイクボタンに、返事の最後が隠れないように余白を空ける
      padding: const EdgeInsets.only(bottom: 80),
      child: SelectableText(_answer), // 選択してコピーできる
    ),
  ),

解説

Gemini とのやり取りは GeminiService にまとめ、画面は「いつ送るか」と「何を表示するか」だけを受け持ちます。画面からは _gemini.ask(質問) を呼ぶだけなので、あとで firebase_ai などに替えるときも、直すのは GeminiService だけで済みます。

画面が持つ状態は次の2つです(音声入力やエラー表示の状態は省略しています)。

変数意味
_isAskingいま返事を待っているか(ボタンを押せなくする・くるくるを出すのに使う)
_answerGemini から届いた返事

GeminiService:APIキーを読んでモデルを作る

最初に .env から APIキーを読み、GenerativeModel にモデル名とキーを渡してモデルを作ります。普段使う flash と、予備の flash-lite の2つを作っておきます。

キーが読めなかったときはモデルを作らず、isConfigured が false になります。.envに書き換え忘れのときも、キーがないものとして扱います。

ask:質問を送って返事を受け取る

Content.text(質問) で質問を包み、generateContent に渡すと、Gemini の返事が返ってきます。返事の文章は response.text に入っています。返事が空のときは null になるので、(応答がありませんでした) と表示します。

generateContent は返事が届くまで時間がかかるので、await で待ちます。待っている間も、画面は固まらずに動き続けます。

_withRetryOnOverload:混雑したら待ってやり直す

Gemini が混雑していると、返事の代わりに「503」「UNAVAILABLE」を含むエラーが返ることがあります。混雑は少し待つと解消することが多いので、2秒待ってから1回だけやり直します。

それでも混雑していたら、ask の側で予備の flash-lite に切り替えて返事を作ります。混雑以外のエラー(キーが間違っているなど)は、待っても直らないので、やり直さずにそのまま画面へ伝えます(rethrow)。

_askGemini:二重送信を防いで送る

_askGemini は、次の順に動きます。

  1. 入力欄が空なら何もしない
  2. 先に _isAsking を true にする。ボタンの onPressed が null になり、押せなくなる
  3. 聞き取り中なら、マイクを止めてから送る(話している途中の文字が混ざらないようにする)
  4. _gemini.ask で返事を待ち、届いたら _answer に入れて表示する
  5. 失敗したら、エラーの文を赤字で表示する
  6. finally で、成功しても失敗しても _isAsking を false に戻す

2 を先にしておかないと、返事を待っている間にボタンを何度も押せてしまい、同じ質問が何回も送られます。

筆者
筆者

送信中に何回も押すと、Gemini の返事でエラーが起きるのではと考えて、送信中はボタンを押せなくしました。

mounted は「この画面がまだ表示されているか」です。返事を待っている間に画面が閉じられていたら、setState を呼ばないようにしています(閉じた画面を更新しようとするとエラーになるため)。

ボタンを押せなくする書き方を解説しています。

返事の表示:スクロールとコピー

返事は長くなりやすいので、SingleChildScrollView に入れてスクロールできるようにします。SelectableText を使うと、返事を選択してコピーできます。

エラーと解決策・対策

以下では、想定しうるエラーの解決方法を紹介します。APIキーを第三者に公開するのは非常に危険ですので、取り扱いには十分に注意してください。

503 UNAVAILABLE(混雑)で返事が来ない

Gemini 側が混雑していると、503(UNAVAILABLE)のエラーが返ります。こちらのコードの誤りではないので、時間をおいてやり直せば返事が来ることが多いです。

筆者
筆者

実際に混雑に当たったときは、ボタンを押し直してもエラーが表示されるだけでした。そこで、同じモデルで粘るより、別のモデルに切り替えて返事をもらう方がよいと考えました。

公式のトラブルシューティングでは、少し待ってからやり直し、やり直すたびに待つ時間を延ばす方法がすすめられています。

本記事のコードは、この考え方を簡単にして「2秒待って1回やり直す」にしています。さらに、やり直してもだめなときは予備のモデルに切り替えるのは、待たされる時間を減らすための工夫です(公式の手順にはありません)。

参考:Troubleshooting guide

返事に「##」や「**」がそのまま出る

完成イメージのように、返事に「##」や「**」がそのまま出ることがあります。Gemini はマークダウン(見出しや太字を記号で表す書き方)で返事を書くことが多く、SelectableText は記号をそのまま文字として表示するためです。

直し方は2通りあります。

  • 表示側で整える: マークダウンを見た目に変えて表示するパッケージを使う。よく紹介される flutter_markdown は開発が終了しているので、後継として案内されている flutter_markdown_plus を使う
  • 記号を使わせない: 質問と一緒に「マークダウンや記号を使わず、普通の文章で答えて」と指示する

返事を読み上げたり吹き出しに出したりする場合は、記号を使わせない方法が向いています。

参考:flutter_markdown_plus

「GEMINI_API_KEY が未設定です」と出る

APIキーが読めていない状態です。次を確認してください。

  • .env がプロジェクトの直下(pubspec.yaml と同じ場所)にあるか
  • pubspec.yaml の flutter: の assets: に .env を書いたか(書き忘れると、アプリに .env が入らない)
  • .env の書き方が GEMINI_API_KEY=キー になっているか(your_api_key_here のままになっていないか)

pubspec.yaml を直したあとは、ホットリロードではなく、アプリを止めてから起動し直してください。

WebではAPIキーが見えてしまう

.env を assets に入れると、Web で公開したときに、.env のファイルがそのまま配信されます。誰でも中の APIキーを読めてしまうので、手元で試すだけにして、build/web を公開しないでください。

Web に限らず、スマホのアプリに入れたキーも、取り出そうと思えば取り出せます。公式も、Web やスマホのアプリにキーを直接入れないこと、本番ではサーバーを間に置いて呼び出すことをすすめています。

人に使ってもらうアプリでは、APIキーをアプリに入れない作りにします。Firebase AI Logic(firebase_ai)に移すか、自分のサーバーを間に置いて Gemini を呼ぶ形にしましょう。

参考:Using Gemini API keys

まとめ

Flutter から Gemini API を使う流れは、モデルを作り、generateContent に質問を渡して、response.text で返事を受け取る、の3つです。

  • Gemini とのやり取りは GeminiService にまとめ、画面は送るタイミングと表示だけを受け持つ
  • 返事を待つ間はボタンを押せなくし、finally で必ず元に戻す
  • 混雑(503)は待ってやり直す。だめなら軽いモデルに切り替える
  • 返事のマークダウン記号は、表示側で整えるか、使わないように指示する
  • google_generative_ai は非推奨。公開するアプリは Firebase AI Logic かサーバー経由にして、APIキーをアプリに入れない

まずは手元で質問を送って返事が返ってくるところまで動かし、AI に何を任せたいかを試してみてください。

タイトルとURLをコピーしました