Gemini Nano in your app without the guesswork: design for 'unavailable' first
Gemini Nano only runs on some phones. Here is how to check its status before every call, what each of the four states means, and the surprises that trip people up once.

Gemini Nano is the language model that ships with the phone. But only some phones. Build the feature as if it is always there and most of your users get an error, or a button that does nothing.
This post covers how to plan for that, using Chapter 3 (Gemini Nano Without the Guesswork) of On-Device AI for Android.
How it works, in one paragraph
Gemini Nano runs inside AICore, an Android system service. AICore keeps the model updated, applies Google's safety filters and lets every app on the device share one copy. You never bundle or host the base model. You reach it through ML Kit's GenAI APIs, which come in two shapes:
- Feature APIs for common jobs: Summarization, Proofreading, Rewriting, Image description and Speech recognition. Each one is tuned for its task and needs no prompt writing.
- The Prompt API for everything else: your own text prompt, or an image plus text, with optional settings such as temperature and maximum output tokens.
The trap: assuming the model is there
Google's ML Kit GenAI overview page publishes the supported device list, and it differs by API. At the time of writing (October 2026) the feature APIs list recent flagships from Google (Pixel 9, 10 and 11 families), Samsung (Galaxy S25 and S26 families and recent foldables) and models from other makers. The Prompt API has its own list, grouped by Gemini Nano version. The list grows, so check it on developers.google.com/ml-kit/genai before you promise a feature.
Two more conditions from the same documentation:
- The GenAI APIs need Android API level 26 or higher.
- They are not supported on devices with an unlocked bootloader.
The book's advice is blunt: design for "unavailable" first. On most of your users' phones today, the Gemini Nano path will report that it cannot run. That has to lead somewhere useful.
The fix: every call starts with a status check
Each GenAI client can report one of four states. Each one has a clear next step:
| State | What it means | What to do |
|---|---|---|
| AVAILABLE | You can run inference now | Run it |
| DOWNLOADABLE | The phone supports it, but the model or the feature's adapter is not on the device yet | Download, with progress |
| DOWNLOADING | A download is in progress | Wait or queue the request |
| UNAVAILABLE | This device cannot run it, or AICore has not fetched its latest configuration yet | Use another backend |
Only UNAVAILABLE sends the user elsewhere. The other three end in AVAILABLE.
The Prompt API, behind an interface
The Prompt API is version 1.0.0-beta4 at the time of writing. Its Kotlin API is coroutine-first: checkStatus() is a suspend function, download() returns a Flow, and generateContentStream() streams chunks.
dependencies {
implementation("com.google.mlkit:genai-prompt:1.0.0-beta4")
}
The book wraps it in a TextGenerator interface, so the status maps to an availability the rest of the app understands:
class NanoGenerator : TextGenerator {
private val model = Generation.getClient()
override val label = "On this phone (Gemini Nano)"
override suspend fun availability(): Availability =
when (model.checkStatus()) {
FeatureStatus.AVAILABLE -> Availability.Ready
FeatureStatus.DOWNLOADABLE,
FeatureStatus.DOWNLOADING -> Availability.NeedsDownload(null)
else -> Availability.Unavailable("Gemini Nano is not supported here")
}
suspend fun downloadModel(onProgress: (Long) -> Unit) {
model.download().collect { s ->
if (s is DownloadStatus.DownloadProgress)
onProgress(s.totalBytesDownloaded)
if (s is DownloadStatus.DownloadFailed) throw s.e
}
}
override fun generate(messages: List<ChatMessage>): Flow<String> {
val prompt = messages.toPlainPrompt() // see below
return model.generateContentStream(prompt)
.map { chunk -> chunk.candidates[0].text }
}
}
The point of the interface is the fallback. When availability() says Unavailable, the app's chooser moves on to a local LiteRT-LM model or a home server, and the screen does not change.
The Prompt API takes one prompt, not a structured chat history, so toPlainPrompt() is yours to write: a short instruction line, then the last few turns as "User:" and "Assistant:" lines. Keep it well under the input limit below.
The easiest win: Summarization
If your feature is "summarize this", start with the Summarization feature API (1.0.0-beta1 at the time of writing). It needs no prompt engineering. The feature APIs return Guava ListenableFutures, so add the coroutines Guava adapter to await them:
dependencies {
implementation("com.google.mlkit:genai-summarization:1.0.0-beta1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.11.0")
}
Create the client once with the input type, the output length (one, two or three bullets) and the language. Check its status with checkFeatureStatus(), and download with downloadFeature() when it reports DOWNLOADABLE. Close the client when the screen goes away; the documentation suggests onCleared() in your ViewModel.
Input must be under 4000 tokens, roughly 3000 English words, for both Summarization and the Prompt API. For longer text, the summarization docs suggest summarizing the first 4000 tokens or turning on automatic truncation with setLongInputAutoTruncationEnabled.
Things that surprise people once
- Fresh devices are not ready. Right after a phone is set up or AICore's data is cleared, AICore may still be initializing and fetching its configuration. The documentation advises retrying later. Treat these errors as "try again in a moment", not as "unsupported forever".
- There is a per-app quota. AICore enforces an inference quota per app. Do not fire a request on every keystroke. Trigger generation on a deliberate action.
- Long output is a bad fit. The docs advise against use cases that need more than 4K tokens of output.
- It is beta. Pin the version, read the release notes on each upgrade, and keep this code behind your interface.
Checklist
- Check the supported device list for the specific API you use.
- Require API level 26+, and expect UNAVAILABLE on unlocked bootloaders.
- Call the status check before every use, and handle all four states.
- Give UNAVAILABLE a real fallback behind one interface.
- Show download progress for DOWNLOADABLE.
- Keep input under 4000 tokens and output use cases short.
- Generate on a deliberate action, not per keystroke.
- Pin the beta versions: genai-prompt 1.0.0-beta4, genai-summarization 1.0.0-beta1.


