取り組みの背景 — なぜ Android アプリに Gemini を組み込むのか
Google が提供する Gemini API は、テキスト生成だけでなく画像認識・音声理解・Function Calling といったマルチモーダル機能を備えた大規模言語モデルです。Android は Google のエコシステムの中心にあるプラットフォームであり、Kotlin と組み合わせることで、ネイティブアプリの中にシームレスに AI 機能を統合できます。
初めて Gemini API を Android で使う方でも、完成形のコードを動かしながら学べる構成にしました。
Gemini API の基本的な概念や料金体系について先に確認しておきたい方は、Gemini API クイックスタート をご覧ください。
前提知識と環境準備
開発環境の要件
本チュートリアルを進めるには、以下の環境が必要です。
Android Studio Ladybug(2025.3)以降
Kotlin 1.9 以降
Android SDK API レベル 21 以上(minSdk)
Firebase プロジェクト (Blaze プラン推奨)
Google AI Studio で取得した API キー、または Firebase コンソールで有効化した Gemini API
Firebase プロジェクトの設定
Firebase AI Logic SDK を使うには、Firebase プロジェクトに Android アプリを登録する必要があります。Firebase コンソールで以下を実施してください。
Firebase コンソール → プロジェクト設定 → 「アプリを追加」で Android アプリを登録
google-services.json をダウンロードし、app/ ディレクトリに配置
Firebase コンソールの「AI Logic」セクションで Gemini API を有効化
Gradle 依存関係の追加
プロジェクトレベルの build.gradle.kts に Firebase BOM と AI Logic SDK を追加します。
// build.gradle.kts (Module: app)
plugins {
id ( "com.android.application" )
id ( "org.jetbrains.kotlin.android" )
id ( "com.google.gms.google-services" )
}
dependencies {
// Firebase BOM で全バージョンを一括管理
implementation ( platform ( "com.google.firebase:firebase-bom:33.12.0" ))
// Firebase AI Logic SDK(Gemini API 連携用)
implementation ( "com.google.firebase:firebase-ai" )
// Coroutines(ストリーミング応答に必要)
implementation ( "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1" )
// Lifecycle ViewModel(UI 統合用)
implementation ( "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7" )
implementation ( "androidx.lifecycle:lifecycle-runtime-ktx:2.8.7" )
}
Sync を実行し、依存関係が正しく解決されることを確認してください。
基本的なテキスト生成 — 最初の Gemini 呼び出し
まずは最もシンプルなテキスト生成から始めましょう。GenerativeModel を初期化し、プロンプトを送信してレスポンスを受け取ります。
import com.google.firebase.ai.FirebaseAI
import com.google.firebase.ai.GenerativeModel
import com.google.firebase.ai.type.GenerativeBackend
// GenerativeModel の初期化
val model: GenerativeModel = FirebaseAI
. getInstance ()
. generativeModel (
modelName = "gemini-3-flash" , // 高速・低コストモデル
backend = GenerativeBackend. googleAI () // Google AI バックエンド
)
// テキスト生成(Coroutine 内で呼び出す)
suspend fun generateResponse (prompt: String ): String {
val response = model. generateContent (prompt)
return response.text ?: "応答を取得できませんでした"
}
// 使用例
// viewModelScope.launch {
// val result = generateResponse("Kotlin の拡張関数を3つ教えてください")
// println(result)
// // 期待出力:
// // 1. String.isEmailValid() - メールアドレスのバリデーション
// // 2. View.visible() - View の表示切り替え
// // 3. Context.toast(message) - Toast の簡易表示
// }
GenerativeBackend.googleAI() は Google AI Studio 経由の直接アクセスです。エンタープライズ環境で VPC やデータレジデンシー要件がある場合は、GenerativeBackend.vertexAI() に切り替えてください。
ストリーミング応答でリアルタイム表示を実装する
チャット UI ではユーザーの待ち時間を短縮するために、トークンが生成されるたびに画面に反映するストリーミング表示が効果的です。Firebase AI Logic SDK は Kotlin の Flow に対応しています。
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import com.google.firebase.ai.type.GenerateContentResponse
fun streamResponse (prompt: String ): Flow < String > = flow {
val stream: Flow < GenerateContentResponse > =
model. generateContentStream (prompt)
stream. collect { chunk ->
chunk.text?. let { text ->
emit (text) // 部分テキストを順次発行
}
}
}
// ViewModel での使用例
class ChatViewModel : ViewModel () {
private val _response = MutableStateFlow ( "" )
val response: StateFlow < String > = _response
fun askGemini (prompt: String ) {
viewModelScope. launch {
_response. value = ""
streamResponse (prompt). collect { partial ->
_response. value += partial
}
}
}
}
// 期待される動作:
// "Kotlin" → "Kotlin は" → "Kotlin は JetBrains が" → ...
// チャンクごとに UI が更新され、応答完了を待たずに表示される
ストリーミング応答の詳細な実装パターンについては、Gemini API ストリーミング応答のチャンク制御とエラー UX で体系的に解説しています。
マルチモーダル入力 — カメラ画像を Gemini で解析する
Gemini の強力な特徴の一つがマルチモーダル対応です。Android のカメラで撮影した画像をそのまま Gemini に送信し、内容を分析させることができます。
import android.graphics.Bitmap
import com.google.firebase.ai.type.content
suspend fun analyzeImage (bitmap: Bitmap , question: String ): String {
// テキストと画像を含むマルチモーダルプロンプトを構築
val inputContent = content {
image (bitmap) // Bitmap を直接渡せる
text (question)
}
val response = model. generateContent (inputContent)
return response.text ?: "画像を分析できませんでした"
}
// 使用例: カメラで撮影した料理の画像を分析
// val result = analyzeImage(
// bitmap = cameraBitmap,
// question = "この料理の名前とおおよそのカロリーを教えてください"
// )
// 期待出力:
// "この画像はカルボナーラです。1人前あたり約650〜800kcalと推定されます。
// 主な材料はパスタ、卵黄、パンチェッタ、パルミジャーノチーズです。"
content {} ビルダーは画像だけでなく、PDF や動画のバイナリデータにも対応しています。大きなファイルを送信する場合は Files API でアップロードしてからファイル参照を渡す方法が効率的です。
Function Calling — アプリの機能を AI から呼び出す
Function Calling を使うと、Gemini が「今の天気を調べたい」といったユーザーの意図を判断し、アプリ側で定義した関数を呼び出すことができます。AI が直接外部 API にアクセスするのではなく、アプリがハブとなって処理を仲介する設計です。
import com.google.firebase.ai.type.FunctionDeclaration
import com.google.firebase.ai.type.Schema
import com.google.firebase.ai.type.Tool
import com.google.firebase.ai.type.FunctionResponse
import com.google.firebase.ai.type.content
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
// 1. 関数の宣言(Gemini に「こういう関数がある」と伝える)
val getWeatherFunc = FunctionDeclaration (
name = "getWeather" ,
description = "指定された都市の現在の天気情報を取得する" ,
parameters = mapOf (
"city" to Schema. string ( "天気を取得する都市名(例: Tokyo, Osaka)" )
)
)
// 2. ツール付きモデルの作成
val modelWithTools = FirebaseAI
. getInstance ()
. generativeModel (
modelName = "gemini-3-flash" ,
backend = GenerativeBackend. googleAI (),
tools = listOf ( Tool ( listOf (getWeatherFunc)))
)
// 3. Function Calling のハンドリング
suspend fun chatWithFunctions (userMessage: String ): String {
val chat = modelWithTools. startChat ()
val response = chat. sendMessage (userMessage)
// Gemini が関数呼び出しを要求した場合
val functionCall = response.functionCalls. firstOrNull ()
if (functionCall != null ) {
// アプリ側で実際の処理を実行
val city = functionCall.args[ "city" ] as ? String ?: "Tokyo"
val weatherData = fetchWeatherFromApi (city) // 自前の API 呼び出し
// 結果を Gemini に返す
val functionResponse = content {
part ( FunctionResponse (
name = "getWeather" ,
response = JsonObject ( mapOf (
"temperature" to JsonPrimitive (weatherData.temp),
"condition" to JsonPrimitive (weatherData.condition)
))
))
}
val finalResponse = chat. sendMessage (functionResponse)
return finalResponse.text ?: ""
}
return response.text ?: ""
}
// 期待出力(「東京の天気を教えて」と入力した場合):
// "東京の現在の天気は晴れ、気温は22°Cです。
// お出かけ日和ですが、夕方から雲が増える予報もあるので折りたたみ傘があると安心です。"
Function Calling の詳しい設計パターンを学びたい方は、Gemini API Function Calling 完全ガイド も併せてご覧ください。
ViewModel + Jetpack Compose で AI チャット UI を構築する
ここまでの要素を組み合わせて、実用的なチャット UI を構築しましょう。MVVM アーキテクチャに沿った設計で、ストリーミング応答をリアルタイム表示します。
// ChatViewModel.kt
class ChatViewModel : ViewModel () {
private val model = FirebaseAI
. getInstance ()
. generativeModel (
modelName = "gemini-3-flash" ,
backend = GenerativeBackend. googleAI ()
)
private val chat = model. startChat ()
data class Message (
val text: String ,
val isUser: Boolean ,
val isStreaming: Boolean = false
)
private val _messages = MutableStateFlow < List < Message >>( emptyList ())
val messages: StateFlow < List < Message >> = _messages
private val _isLoading = MutableStateFlow ( false )
val isLoading: StateFlow < Boolean > = _isLoading
fun sendMessage (userText: String ) {
// ユーザーメッセージを追加
_messages. value += Message (text = userText, isUser = true )
_isLoading. value = true
viewModelScope. launch {
try {
// ストリーミング応答の受信
var aiResponse = ""
_messages. value += Message (
text = "" , isUser = false , isStreaming = true
)
chat. sendMessageStream (userText). collect { chunk ->
chunk.text?. let { partial ->
aiResponse += partial
// 最後のメッセージを更新
_messages. value = _messages. value . dropLast ( 1 ) +
Message (
text = aiResponse,
isUser = false ,
isStreaming = true
)
}
}
// ストリーミング完了
_messages. value = _messages. value . dropLast ( 1 ) +
Message (text = aiResponse, isUser = false )
} catch (e: Exception ) {
_messages. value += Message (
text = "エラーが発生しました: ${e.localizedMessage}" ,
isUser = false
)
} finally {
_isLoading. value = false
}
}
}
}
この ViewModel を Jetpack Compose の @Composable 関数から collectAsState() で監視すれば、メッセージの追加やストリーミング更新がリアクティブに UI へ反映されます。
エラーハンドリングとプロダクション品質のベストプラクティス
本番環境で安定して動作させるために、以下のポイントを押さえましょう。
レート制限とリトライ
import kotlinx.coroutines.delay
suspend fun < T > retryWithBackoff (
maxRetries: Int = 3 ,
initialDelay: Long = 1000L ,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat (maxRetries - 1 ) { attempt ->
try {
return block ()
} catch (e: Exception ) {
// 429 (Rate Limit) や 503 (Service Unavailable) のリトライ
if (e.message?. contains ( "429" ) == true ||
e.message?. contains ( "503" ) == true ) {
delay (currentDelay)
currentDelay *= 2 // 指数バックオフ
} else {
throw e // その他のエラーは即座に再スロー
}
}
}
return block () // 最後の試行
}
// 使用例
// val result = retryWithBackoff {
// model.generateContent("プロンプト")
// }
Safety Settings の設定
ユーザー入力を直接 Gemini に送信する場合は、不適切なコンテンツの生成を防ぐために Safety Settings を明示的に設定してください。
import com.google.firebase.ai.type.HarmCategory
import com.google.firebase.ai.type.HarmBlockThreshold
import com.google.firebase.ai.type.SafetySetting
val safeModel = FirebaseAI
. getInstance ()
. generativeModel (
modelName = "gemini-3-flash" ,
backend = GenerativeBackend. googleAI (),
safetySettings = listOf (
SafetySetting (HarmCategory.HARASSMENT, HarmBlockThreshold.MEDIUM_AND_ABOVE),
SafetySetting (HarmCategory.HATE_SPEECH, HarmBlockThreshold.MEDIUM_AND_ABOVE),
SafetySetting (HarmCategory.SEXUALLY_EXPLICIT, HarmBlockThreshold.MEDIUM_AND_ABOVE),
SafetySetting (HarmCategory.DANGEROUS_CONTENT, HarmBlockThreshold.MEDIUM_AND_ABOVE)
)
)
API キーの管理やプロンプトインジェクション対策など、セキュリティ面をさらに強化したい方には Gemini API 本番環境セキュリティ完全ガイド が参考になります。
個人開発アプリに組み込んで分かった、本番運用の勘所
私自身、個人開発で運用している Android アプリにこの構成を載せてみて、サンプルコードのままでは詰まる箇所がいくつかあることに気づきました。
とくに体感を左右したのは、最初のトークンが返るまでの時間です。gemini-3-flash を Google AI バックエンド経由で呼んだとき、手元のミドルレンジ実機・モバイル回線ではおよそ 0.8〜1.4 秒。この空白を無言で待たせると「固まった」と受け取られてしまいます。送信直後にタイピングインジケーターを出すだけで、体感的な不満が目に見えて減りました。AI 機能の第一印象は、最初の一秒で決まる。地味ですが、確かな手応えのある気づきでした。
もう一つ、見落としやすいのがコストです。チャット機能では会話履歴がそのまま入力トークンとして積み上がっていきます。startChat() で履歴を保持したまま 20 往復もすると、1 リクエストあたりの入力トークンが初回の数倍に膨らんでいることも珍しくありません。月あたりの想定リクエスト数に「平均入力トークン × 単価」を掛けて概算するだけで、設計段階で青ざめずに済みます。下のセクションで、その釣り合いの取り方を整理します。
モデルを使い分けて応答品質とコストの釣り合いを取る
すべての機能を最上位モデルで動かす必要はありません。私は「ユーザーが待てる時間」と「求められる精度」の二軸で割り当てを決めています。
入力補完・要約・短い相槌のような軽いタスク → gemini-3-flash(低レイテンシ・低コスト)
画像の詳細な読み取りや、長い文脈を踏まえた判断が必要なタスク → gemini-3-pro(精度優先)
コード側では、機能ごとにモデルを切り替える薄いファクトリを一枚挟んでおくと、後からの調整が楽になります。
enum class TaskTier { LIGHT, HEAVY }
fun modelFor (tier: TaskTier ): GenerativeModel {
val name = when (tier) {
TaskTier.LIGHT -> "gemini-3-flash" // 低レイテンシ・低コスト
TaskTier.HEAVY -> "gemini-3-pro" // 精度優先
}
return FirebaseAI. getInstance (). generativeModel (
modelName = name,
backend = GenerativeBackend. googleAI ()
)
}
// 使用例: 軽い相槌は flash、画像の精読は pro に振り分ける
// val quick = modelFor(TaskTier.LIGHT)
// val vision = modelFor(TaskTier.HEAVY)
この一枚があるだけで、リリース後に「この機能だけ pro に上げたい」「ここは flash で十分だった」と気づいたときの変更が、呼び出し箇所を一つ直すだけで済みます。最新モデルの選び分けについては、Gemini 2.5 Pro 最新 API 実践開発ガイド も判断材料になります。
オンデバイス推論をフォールバックに据え、オフラインと急なコスト増に備える
モバイルアプリでは、地下鉄や圏外で API に届かない瞬間が必ず訪れます。Google Play で配信している個人開発アプリでも、この圏外時の挙動がレビュー評価を地味に左右しました。私は軽量タスクに限り、端末内の Gemma を第二の経路として用意しておく構成を好んでいます。クラウドが使えないときも、要約や定型応答だけは手元で返せるようにしておくと、体験が途切れません。
suspend fun generateWithFallback (prompt: String ): String {
return try {
// 第一経路: クラウドの Gemini
model. generateContent (prompt).text ?: onDeviceFallback (prompt)
} catch (e: Exception ) {
// ネットワーク不通・レート制限時はオンデバイスへ退避
onDeviceFallback (prompt)
}
}
// onDeviceFallback は端末内の Gemma 等で最低限の応答を返す
// (精度よりも「無言で失敗しない」ことを優先する経路)
オンデバイス側の具体的な組み込み方は、Android Studio で Gemma 4 をオンデバイス AI アシスタントとして組み込む で詳しく扱っています。クラウドとローカルを役割で分ける発想は、コストの急変動に対する保険にもなります。
使用状況を計測して、チューニングの土台を作る
「なんとなく速い・遅い」「なんとなく高い」で運用を続けると、改善の打ち手が勘頼みになります。応答に含まれる usageMetadata を毎回ログに残し、入力・出力トークンを蓄積するだけで、判断が数字に変わります。
各レスポンスから usageMetadata を取り出す
機能名・トークン数・所要時間をローカルに記録する
週次で集計し、トークンが膨らんでいる機能を特定する
該当機能のプロンプト短縮・履歴の打ち切り・モデル変更を検討する
suspend fun generateAndLog (feature: String , prompt: String ): String {
val started = System. currentTimeMillis ()
val response = model. generateContent (prompt)
val usage = response.usageMetadata
val elapsed = System. currentTimeMillis () - started
// 計測値を蓄積(実際は Room などに保存して週次集計する)
android.util.Log. d (
"GeminiUsage" ,
"feature= $feature input=${usage?.promptTokenCount} " +
"output=${usage?.candidatesTokenCount} elapsedMs= $elapsed "
)
return response.text ?: ""
}
私の場合、この記録を二週間続けただけで、「画像解析より会話履歴の肥大のほうがトークンを食っている」という、着手前の予想とは逆の事実が見えてきました。計測は、思い込みを正すために回すものだと考えています。
次の一歩
まずは gemini-3-flash での最小チャットを一画面だけ動かし、その応答に含まれる usageMetadata をログに出すところまでを今日のゴールにしてみてください。数字が出た瞬間に、次に削るべき場所が自然と見えてきます。
セキュリティ面をさらに固めたい場合は、Gemini API 本番環境セキュリティ完全ガイド へ進むのがおすすめです。
同じように個人開発で AI 機能を育てている方の役に立てば嬉しいです。お読みいただきありがとうございました。