●SUNSET — Five days until the imagen-4.0 family shuts down: imagen-4.0-generate-001 and two sibling models stop on August 17●SAMPLING — Since July 21 the temperature, top_p, and top_k parameters are deprecated; no shutdown date yet, but worth auditing your calls●API — The Interactions API is now generally available and is the recommended path to the latest models and features●ROBOTICS — gemini-robotics-er-1.6-preview shuts down on August 31; its successor, ER 2, has been in public preview since July 30●ENTERPRISE — The Gemini Enterprise mobile app reached GA for organizations authenticating through third-party identity providers●RENAME — NotebookLM Enterprise is now Gemini Notebook Enterprise, so the term to search the docs for has changed too●SUNSET — Five days until the imagen-4.0 family shuts down: imagen-4.0-generate-001 and two sibling models stop on August 17●SAMPLING — Since July 21 the temperature, top_p, and top_k parameters are deprecated; no shutdown date yet, but worth auditing your calls●API — The Interactions API is now generally available and is the recommended path to the latest models and features●ROBOTICS — gemini-robotics-er-1.6-preview shuts down on August 31; its successor, ER 2, has been in public preview since July 30●ENTERPRISE — The Gemini Enterprise mobile app reached GA for organizations authenticating through third-party identity providers●RENAME — NotebookLM Enterprise is now Gemini Notebook Enterprise, so the term to search the docs for has changed too
Building AI-Powered Android Apps with Gemini API and Kotlin
Integrate Gemini API into a Kotlin Android app with the Firebase AI Logic SDK — from setup through production realities: measured first-token latency, model tiering, on-device fallback, and cost estimation, with notes from real indie-app use.
Google's Gemini API offers much more than text generation — it supports image recognition, audio understanding, Function Calling, and other multimodal capabilities through a single unified API. As the center of Google's ecosystem, Android provides the most natural platform for integrating Gemini into native mobile experiences.
This guide walks you through the entire process of adding Gemini to a Kotlin-based Android app using the Firebase AI Logic SDK (formerly Firebase Vertex AI SDK). From initial setup to production-quality streaming chat, you'll have working code at every step.
If you'd like a general overview of the Gemini API before diving in, check out [Gemini API Quickstart]((/articles/gemini-api/gemini-api-quickstart).
Prerequisites and Environment Setup
Development Requirements
To follow along, you'll need:
Android Studio Ladybug (2025.3) or later
Kotlin 1.9+
Android SDK API level 21+ (minSdk)
Firebase project (Blaze plan recommended)
API key from Google AI Studio, or Gemini API enabled in your Firebase console
Setting Up Your Firebase Project
The Firebase AI Logic SDK requires a Firebase project with your Android app registered. In the Firebase Console:
Go to Project Settings → "Add app" and register your Android app
Download google-services.json and place it in your app/ directory
Navigate to the "AI Logic" section and enable the Gemini API
Adding Gradle Dependencies
Add the Firebase BOM and AI Logic SDK to your module-level build.gradle.kts:
// build.gradle.kts (Module: app)plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("com.google.gms.google-services")}dependencies { // Firebase BOM manages all Firebase library versions implementation(platform("com.google.firebase:firebase-bom:33.12.0")) // Firebase AI Logic SDK for Gemini API integration implementation("com.google.firebase:firebase-ai") // Coroutines for streaming responses implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1") // Lifecycle ViewModel for UI integration implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")}
Run a Gradle sync to make sure all dependencies resolve correctly.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A minimal Kotlin-to-Gemini implementation via Firebase AI Logic SDK, plus real code for streaming, multimodal, and Function Calling
✦Measured first-token latency on a real device, and how to tier flash vs pro per feature to balance cost and quality
✦A hybrid on-device fallback for offline and cost spikes, and a usage-metering routine to ground your tuning in numbers
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Let's start with the simplest possible interaction: sending a text prompt and receiving a response. Initialize a GenerativeModel instance and call generateContent.
import com.google.firebase.ai.FirebaseAIimport com.google.firebase.ai.GenerativeModelimport com.google.firebase.ai.type.GenerativeBackend// Initialize the GenerativeModelval model: GenerativeModel = FirebaseAI .getInstance() .generativeModel( modelName = "gemini-3-flash", // Fast, cost-effective model backend = GenerativeBackend.googleAI() // Google AI backend )// Generate text (call within a coroutine)suspend fun generateResponse(prompt: String): String { val response = model.generateContent(prompt) return response.text ?: "Failed to get a response"}// Usage example:// viewModelScope.launch {// val result = generateResponse("Give me 3 useful Kotlin extension functions")// println(result)// // Expected output:// // 1. String.isEmailValid() - Email validation// // 2. View.visible() - Toggle view visibility// // 3. Context.toast(message) - Quick Toast display// }
GenerativeBackend.googleAI() connects directly through Google AI Studio. If your organization requires VPC or data residency controls, switch to GenerativeBackend.vertexAI().
Streaming Responses for Real-Time Display
In a chat interface, streaming tokens as they're generated dramatically improves perceived responsiveness. The Firebase AI Logic SDK supports Kotlin's Flow for streaming.
import kotlinx.coroutines.flow.Flowimport kotlinx.coroutines.flow.flowimport com.google.firebase.ai.type.GenerateContentResponsefun streamResponse(prompt: String): Flow<String> = flow { val stream: Flow<GenerateContentResponse> = model.generateContentStream(prompt) stream.collect { chunk -> chunk.text?.let { text -> emit(text) // Emit partial text as it arrives } }}// ViewModel usageclass 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 } } }}// Expected behavior:// "Kotlin" → "Kotlin is" → "Kotlin is a modern" → ...// UI updates incrementally without waiting for the full response
For deeper coverage of streaming patterns, see [Gemini API Streaming × Function Calling Integration Guide]((/articles/gemini-api/gemini-api-streaming-response-control-chunk-error-ux).
Multimodal Input — Analyzing Camera Images with Gemini
One of Gemini's standout features is multimodal support. You can send images captured by the device camera directly to Gemini for analysis.
import android.graphics.Bitmapimport com.google.firebase.ai.type.contentsuspend fun analyzeImage(bitmap: Bitmap, question: String): String { // Build a multimodal prompt with text and image val inputContent = content { image(bitmap) // Pass the Bitmap directly text(question) } val response = model.generateContent(inputContent) return response.text ?: "Could not analyze the image"}// Example: Analyzing a photo of food// val result = analyzeImage(// bitmap = cameraBitmap,// question = "What dish is this and roughly how many calories does it have?"// )// Expected output:// "This appears to be Carbonara. A typical serving contains approximately// 650-800 calories. Key ingredients include pasta, egg yolk, pancetta,// and Parmigiano-Reggiano cheese."
The content {} builder also accepts PDF and video binary data. For larger files, consider using the Files API to upload first and then pass the file reference.
Function Calling — Letting AI Invoke App Features
Function Calling allows Gemini to recognize user intent (like "check the weather") and request your app to execute a specific function. The AI doesn't call external APIs directly — your app acts as the intermediary.
import com.google.firebase.ai.type.FunctionDeclarationimport com.google.firebase.ai.type.Schemaimport com.google.firebase.ai.type.Toolimport com.google.firebase.ai.type.FunctionResponseimport com.google.firebase.ai.type.contentimport kotlinx.serialization.json.JsonObjectimport kotlinx.serialization.json.JsonPrimitive// 1. Declare the function (tell Gemini what's available)val getWeatherFunc = FunctionDeclaration( name = "getWeather", description = "Get current weather information for a specified city", parameters = mapOf( "city" to Schema.string("City name to get weather for (e.g., Tokyo, New York)") ))// 2. Create model with toolsval modelWithTools = FirebaseAI .getInstance() .generativeModel( modelName = "gemini-3-flash", backend = GenerativeBackend.googleAI(), tools = listOf(Tool(listOf(getWeatherFunc))) )// 3. Handle Function Calling in the conversationsuspend fun chatWithFunctions(userMessage: String): String { val chat = modelWithTools.startChat() val response = chat.sendMessage(userMessage) // Check if Gemini requested a function call val functionCall = response.functionCalls.firstOrNull() if (functionCall != null) { // Execute the actual logic on the app side val city = functionCall.args["city"] as? String ?: "Tokyo" val weatherData = fetchWeatherFromApi(city) // Your own API call // Send the result back to 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 ?: ""}// Expected output (when user asks "What's the weather in Tokyo?"):// "The current weather in Tokyo is sunny with a temperature of 22°C.// It's a great day to be outside, though clouds may move in by evening,// so you might want to carry a small umbrella."
To explore advanced Function Calling design patterns, see [Gemini API Function Calling Complete Guide]((/articles/gemini-api/gemini-api-function-calling-complete).
Building a Chat UI with ViewModel and Jetpack Compose
Now let's bring everything together into a practical chat interface. This follows the MVVM architecture with real-time streaming display.
// ChatViewModel.ktclass 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) { // Add user message _messages.value += Message(text = userText, isUser = true) _isLoading.value = true viewModelScope.launch { try { // Receive streaming response var aiResponse = "" _messages.value += Message( text = "", isUser = false, isStreaming = true ) chat.sendMessageStream(userText).collect { chunk -> chunk.text?.let { partial -> aiResponse += partial // Update the last message _messages.value = _messages.value.dropLast(1) + Message( text = aiResponse, isUser = false, isStreaming = true ) } } // Mark streaming as complete _messages.value = _messages.value.dropLast(1) + Message(text = aiResponse, isUser = false) } catch (e: Exception) { _messages.value += Message( text = "An error occurred: ${e.localizedMessage}", isUser = false ) } finally { _isLoading.value = false } } }}
Observe this ViewModel from a @Composable function using collectAsState(), and message additions and streaming updates will reactively reflect in your UI.
Error Handling and Production Best Practices
To keep your app running smoothly in production, here are essential patterns to implement.
Rate Limiting and Retry Logic
import kotlinx.coroutines.delaysuspend 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) { // Retry on 429 (Rate Limit) or 503 (Service Unavailable) if (e.message?.contains("429") == true || e.message?.contains("503") == true) { delay(currentDelay) currentDelay *= 2 // Exponential backoff } else { throw e // Rethrow other errors immediately } } } return block() // Final attempt}// Usage:// val result = retryWithBackoff {// model.generateContent("Your prompt here")// }
Configuring Safety Settings
When passing user input directly to Gemini, explicitly set Safety Settings to prevent inappropriate content generation.
What I Learned Shipping This Into a Real Indie App
Putting this exact setup into an Android app I run solo as an indie developer, I found a few spots where the sample code alone leaves you stuck.
The one that shaped perceived quality most was time-to-first-token. Calling gemini-3-flash through the Google AI backend on a mid-range device over mobile data, the first chunk arrived in roughly 0.8 to 1.4 seconds. Leave that gap silent and users read it as "frozen." Simply showing a typing indicator the instant a message is sent visibly cut the complaints. A first impression of an AI feature is decided in the first second — a small but solid lesson.
The other easy thing to miss is cost. In a chat feature, conversation history piles up as input tokens. Keep history with startChat() for twenty turns and per-request input tokens are often several times the first call. Multiplying your expected monthly requests by "average input tokens x unit price" before you build saves you from a nasty surprise later. The next section lays out how I keep that balance.
Tier Your Models to Balance Quality Against Cost
Not every feature needs the top model. I assign models along two axes: how long the user can wait, and how much accuracy the task demands.
Tasks needing fine image reading or long-context judgment -> gemini-3-pro (accuracy first)
A thin factory that swaps models per feature makes later tuning painless.
enum class TaskTier { LIGHT, HEAVY }fun modelFor(tier: TaskTier): GenerativeModel { val name = when (tier) { TaskTier.LIGHT -> "gemini-3-flash" // low latency, low cost TaskTier.HEAVY -> "gemini-3-pro" // accuracy first } return FirebaseAI.getInstance().generativeModel( modelName = name, backend = GenerativeBackend.googleAI() )}// Route light chatter to flash, careful image reading to pro:// val quick = modelFor(TaskTier.LIGHT)// val vision = modelFor(TaskTier.HEAVY)
With that single layer in place, realizing post-launch that "only this feature should be pro" or "flash was plenty here" becomes a one-line change. For choosing among the latest models, Gemini 2.5 Pro Practical API Developer Guide is a useful reference.
Use On-Device Inference as a Fallback for Offline and Cost Spikes
On mobile, moments without connectivity are inevitable — a subway, a dead zone. On the app I ship to Google Play, this offline behavior quietly moved my review ratings. For light tasks only, I like keeping an on-device Gemma as a second path, so summaries and canned replies still come back when the cloud is unreachable and the experience never stalls.
suspend fun generateWithFallback(prompt: String): String { return try { // Primary: cloud Gemini model.generateContent(prompt).text ?: onDeviceFallback(prompt) } catch (e: Exception) { // Fall back on network loss or rate limiting onDeviceFallback(prompt) }}// onDeviceFallback returns a minimal response via on-device Gemma, etc.// (a path that prioritizes "never fail silently" over peak accuracy)
Running on "feels fast / feels slow / feels expensive" leaves your improvements to guesswork. Logging the usageMetadata on every response and accumulating input and output tokens turns judgment into numbers.
Pull usageMetadata from each response
Record feature name, token counts, and elapsed time locally
Aggregate weekly to find features whose tokens are ballooning
Consider prompt trimming, history truncation, or a model change for those features
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 // Accumulate metrics (store in Room, aggregate weekly in practice) android.util.Log.d( "GeminiUsage", "feature=$feature input=${usage?.promptTokenCount} " + "output=${usage?.candidatesTokenCount} elapsedMs=$elapsed" ) return response.text ?: ""}
In my case, just two weeks of this logging revealed that growing conversation history — not image analysis — was eating the most tokens, the opposite of what I had assumed going in. Metering is what you run to correct your own guesses.
Your Next Step
Make today's goal small: get a single-screen chat running on gemini-3-flash and log the usageMetadata from its responses. The moment real numbers appear, the next thing to trim tends to reveal itself.
I hope this helps anyone else growing AI features in a solo project. Thanks for reading.
Share
Thank You for Reading
Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.