入力欄に書いた質問(マイクで話した質問も)を 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 に伝えるための合言葉)が必要です。
- Google AI Studio を開き、左下の鍵のアイコン(Get API key)を押す
- 「API キー」のページで、右上の API キーを作成 を押す
- 作ったキーをコピーしておく

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


無料枠でも、基本的なテストで使う程度なら、制限に当たることはほとんどありません。安心してFlutterと連携してください。
APIキーはパスワードと同じように扱います。GitHub などに上げないようにしてください。
パッケージの追加と.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は非推奨|これから始めるならFirebase AI Logic
本記事のコードは、まず手元で Gemini を試すために google_generative_ai で作りました。いまも動きますが、これから作り始めるアプリ、特に人に公開するアプリでは、後継の Firebase AI Logic(firebase_ai パッケージ)を使うのがおすすめです。
| 項目 | google_generative_ai | firebase_ai(Firebase AI Logic) |
|---|---|---|
| 状態 | 非推奨。今後は変更されない | 開発が続いている |
| 準備 | APIキーだけ | Firebase のプロジェクトが必要 |
| アプリを守る仕組み | なし | App Check(正規のアプリからの呼び出しかを確かめる仕組み)を強く推奨 |
| 呼び出し方 | GenerativeModel(…) を作り、generateContent | FirebaseAI.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 APIに質問し返答する

ここまでの内容をもとに、次の動きをする画面を作ります。
- 入力欄に質問を書く(マイクで話して入れてもよい)
- 「Geminiに聞く」を押すと、質問を Gemini に送る
- 返事を待つ間は、ボタンにくるくるを出して押せなくする
- 返事が届いたら、入力欄の下に表示する(長い返事はスクロールできる)
- 混雑していたら、少し待ってやり直し、それでもだめなら予備のモデルで返事を作る
マイクで話した内容を入力欄に入れる部分は、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 | いま返事を待っているか(ボタンを押せなくする・くるくるを出すのに使う) |
| _answer | Gemini から届いた返事 |
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 は、次の順に動きます。
- 入力欄が空なら何もしない
- 先に _isAsking を true にする。ボタンの onPressed が null になり、押せなくなる
- 聞き取り中なら、マイクを止めてから送る(話している途中の文字が混ざらないようにする)
- _gemini.ask で返事を待ち、届いたら _answer に入れて表示する
- 失敗したら、エラーの文を赤字で表示する
- finally で、成功しても失敗しても _isAsking を false に戻す
2 を先にしておかないと、返事を待っている間にボタンを何度も押せてしまい、同じ質問が何回も送られます。

送信中に何回も押すと、Gemini の返事でエラーが起きるのではと考えて、送信中はボタンを押せなくしました。
mounted は「この画面がまだ表示されているか」です。返事を待っている間に画面が閉じられていたら、setState を呼ばないようにしています(閉じた画面を更新しようとするとエラーになるため)。
返事の表示:スクロールとコピー
返事は長くなりやすいので、SingleChildScrollView に入れてスクロールできるようにします。SelectableText を使うと、返事を選択してコピーできます。
エラーと解決策・対策
以下では、想定しうるエラーの解決方法を紹介します。APIキーを第三者に公開するのは非常に危険ですので、取り扱いには十分に注意してください。
503 UNAVAILABLE(混雑)で返事が来ない
Gemini 側が混雑していると、503(UNAVAILABLE)のエラーが返ります。こちらのコードの誤りではないので、時間をおいてやり直せば返事が来ることが多いです。

実際に混雑に当たったときは、ボタンを押し直してもエラーが表示されるだけでした。そこで、同じモデルで粘るより、別のモデルに切り替えて返事をもらう方がよいと考えました。
公式のトラブルシューティングでは、少し待ってからやり直し、やり直すたびに待つ時間を延ばす方法がすすめられています。
本記事のコードは、この考え方を簡単にして「2秒待って1回やり直す」にしています。さらに、やり直してもだめなときは予備のモデルに切り替えるのは、待たされる時間を減らすための工夫です(公式の手順にはありません)。
返事に「##」や「**」がそのまま出る
完成イメージのように、返事に「##」や「**」がそのまま出ることがあります。Gemini はマークダウン(見出しや太字を記号で表す書き方)で返事を書くことが多く、SelectableText は記号をそのまま文字として表示するためです。
直し方は2通りあります。
- 表示側で整える: マークダウンを見た目に変えて表示するパッケージを使う。よく紹介される flutter_markdown は開発が終了しているので、後継として案内されている 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キーが見えてしまう
Web に限らず、スマホのアプリに入れたキーも、取り出そうと思えば取り出せます。公式も、Web やスマホのアプリにキーを直接入れないこと、本番ではサーバーを間に置いて呼び出すことをすすめています。
人に使ってもらうアプリでは、APIキーをアプリに入れない作りにします。Firebase AI Logic(firebase_ai)に移すか、自分のサーバーを間に置いて Gemini を呼ぶ形にしましょう。
まとめ
Flutter から Gemini API を使う流れは、モデルを作り、generateContent に質問を渡して、response.text で返事を受け取る、の3つです。
- Gemini とのやり取りは GeminiService にまとめ、画面は送るタイミングと表示だけを受け持つ
- 返事を待つ間はボタンを押せなくし、finally で必ず元に戻す
- 混雑(503)は待ってやり直す。だめなら軽いモデルに切り替える
- 返事のマークダウン記号は、表示側で整えるか、使わないように指示する
- google_generative_ai は非推奨。公開するアプリは Firebase AI Logic かサーバー経由にして、APIキーをアプリに入れない
まずは手元で質問を送って返事が返ってくるところまで動かし、AI に何を任せたいかを試してみてください。



