Free weekly email: the week's new posts, videos and one tip from the books, every Sunday. Get it free →
Homelab the Right Way

Home › Posts

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.

· 4 min read · androidgemini nanoml kiton-device aikotlin

Gemini Nano in your app without the guesswork: design for 'unavailable' first

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:

StateWhat it meansWhat to do
AVAILABLEYou can run inference nowRun it
DOWNLOADABLEThe phone supports it, but the model or the feature's adapter is not on the device yetDownload, with progress
DOWNLOADINGA download is in progressWait or queue the request
UNAVAILABLEThis device cannot run it, or AICore has not fetched its latest configuration yetUse 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.

More articles