๐Ÿค– Android AI and Generative AI

Gemini Nano and ML Kit GenAI on Android: How On-Device AI Works

Gemini Nano lets Android apps summarise, proofread, rewrite and describe images entirely on the device, with no network and no per-request fee. Here's how AICore and the ML Kit GenAI APIs work, a complete Kotlin summarisation example, and the limits you must design around.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 6 min read ๐ŸŽฏ Intermediate
On this page
  1. How on-device generative AI works on Android
  2. The ML Kit GenAI APIs at a glance
  3. Implementation: on-device summarisation in Kotlin
  4. The limits that shape your design
  5. When to use the Prompt API instead
  6. Real-world use cases
  7. Troubleshooting
  8. Frequently asked questions
  9. Conclusion

Imagine a notes app that summarises a long meeting note while the user is offline on a flight, without sending a single word to a server and without costing you anything per request. That's what on-device generative AI on Android makes possible, on supported devices and within some important limits.

This article explains how Gemini Nano, AICore and the ML Kit GenAI APIs fit together, walks through a complete Kotlin implementation of on-device summarisation with feature checks, model download and streaming, and covers the constraints (device support, foreground-only use, quotas, input limits) that determine whether this approach fits your feature.

Status at the time of writing

Google's documentation lists the ML Kit GenAI APIs as beta: Summarization, Proofreading, Rewriting, Image Description and Prompt. Speech Recognition is alpha. Beta APIs carry no SLA and can change, so check the official pages before relying on the exact names below.

How on-device generative AI works on Android

Android on-device generative AI stack Top: your app. Below it, the ML Kit GenAI APIs: Summarization, Proofreading, Rewriting, Image Description, Speech Recognition and Prompt. Below them, AICore, the Android system service that manages Gemini Nano and safety features. At the bottom, device hardware: NPU, GPU and CPU. Model downloads come through Private Compute Services. Your app (Kotlin, Compose) ML Kit GenAI APIsSummarization ยท Proofreading ยท Rewriting ยท Image Description ยท Speech ยท Prompt AICore system service ยท Gemini Nanomodel distribution & updates ยท isolated ยท no direct internet access Device hardware: NPU / GPU / CPU PrivateCompute Svc
Your app talks only to ML Kit. AICore owns the model, so you never bundle or update gigabytes of weights yourself, and apps on the device share one copy.
  • Gemini Nano is Google's on-device foundation model.
  • AICore is the Android system service that distributes and updates Gemini Nano and uses device hardware for fast inference. According to Android's documentation, it's isolated from most packages, has no direct internet access, and doesn't store input data or outputs after processing.
  • ML Kit GenAI APIs are the developer-facing layer. The task-specific APIs (summarise, proofread, rewrite, describe images) come with tuned prompts and built-in quality work. The Prompt API accepts your own prompts for custom tasks.

The ML Kit GenAI APIs at a glance

APIWhat it doesGood fit
SummarizationSummarises articles or conversations as bullet pointsLong notes, chat threads, articles
ProofreadingFixes grammar and spelling in short textMessage composers, comments
RewritingRewrites short text in a different tone or style"Make it more formal", "shorten"
Image DescriptionGenerates a short description of an imageAlt text, accessibility, cataloguing
Speech RecognitionTranscribes spoken audio (alpha)Voice notes, dictation
PromptCustom text or multimodal prompts to Gemini NanoClassification, extraction, custom short generation

Implementation: on-device summarisation in Kotlin

The flow is the same for every task-specific API: create a client โ†’ check feature status โ†’ download if needed โ†’ run inference โ†’ close the client.

Feature status flow for ML Kit GenAI checkFeatureStatus returns one of four values. UNAVAILABLE: hide the feature or use a fallback. DOWNLOADABLE: call downloadFeature and show progress. DOWNLOADING: wait, the request runs when ready. AVAILABLE: run inference. checkFeatureStatus() UNAVAILABLEโ†’ hide feature or use cloud fallback (with consent) DOWNLOADABLEโ†’ downloadFeature(), show progress DOWNLOADINGโ†’ wait; the request runs once ready AVAILABLEโ†’ runInference()
Always check status before showing any AI UI. On many devices the answer will be UNAVAILABLE.

1. Add the dependency

app/build.gradle.ktsGradle KTS
dependencies {
    // Beta at the time of writing: check the ML Kit release notes for the current version.
    implementation("com.google.mlkit:genai-summarization:1.0.0-beta1")
    // await() for ListenableFuture, used by the GenAI APIs
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:<latest>")
}

2. Wrap the API behind your own interface

Wrapping keeps beta API details in one file, makes the ViewModel testable with a fake, and lets you add a cloud fallback later without touching the UI.

OnDeviceSummarizer.ktKotlin
import android.content.Context
import com.google.mlkit.genai.common.DownloadCallback
import com.google.mlkit.genai.common.FeatureStatus
import com.google.mlkit.genai.common.GenAiException
import com.google.mlkit.genai.summarization.Summarization
import com.google.mlkit.genai.summarization.SummarizationRequest
import com.google.mlkit.genai.summarization.SummarizerOptions
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.guava.await

sealed interface ModelState {
    data object Unavailable : ModelState
    data object NeedsDownload : ModelState
    data class Downloading(val downloadedBytes: Long, val totalBytes: Long) : ModelState
    data object Ready : ModelState
}

class OnDeviceSummarizer(context: Context) : AutoCloseable {

    private val summarizer = Summarization.getClient(
        SummarizerOptions.builder(context)
            .setInputType(SummarizerOptions.InputType.ARTICLE)
            .setOutputType(SummarizerOptions.OutputType.THREE_BULLETS)
            .setLanguage(SummarizerOptions.Language.ENGLISH)
            .build()
    )

    suspend fun status(): ModelState = when (summarizer.checkFeatureStatus().await()) {
        FeatureStatus.AVAILABLE -> ModelState.Ready
        FeatureStatus.DOWNLOADABLE -> ModelState.NeedsDownload
        FeatureStatus.DOWNLOADING -> ModelState.Downloading(0, 0)
        else -> ModelState.Unavailable
    }

    /** Emits download progress, then Ready. Fails the flow if the download fails. */
    fun download(): Flow<ModelState> = callbackFlow {
        var total = 0L
        summarizer.downloadFeature(object : DownloadCallback {
            override fun onDownloadStarted(bytesToDownload: Long) {
                total = bytesToDownload
                trySend(ModelState.Downloading(0, total))
            }
            override fun onDownloadProgress(totalBytesDownloaded: Long) {
                trySend(ModelState.Downloading(totalBytesDownloaded, total))
            }
            override fun onDownloadCompleted() {
                trySend(ModelState.Ready)
                close()
            }
            override fun onDownloadFailed(e: GenAiException) {
                close(e)
            }
        })
        awaitClose { }
    }

    /** Streams partial text through [onPartial] and returns the final summary. */
    suspend fun summarize(text: String, onPartial: (String) -> Unit): String {
        val request = SummarizationRequest.builder(text).build()
        val result = summarizer.runInference(request) { newText -> onPartial(newText) }.await()
        return result.summary
    }

    override fun close() = summarizer.close()
}

3. Drive it from a ViewModel

SummaryViewModel.ktKotlin
data class SummaryUiState(
    val model: ModelState = ModelState.Unavailable,
    val summary: String = "",
    val isSummarizing: Boolean = false,
    val error: String? = null,
)

class SummaryViewModel(private val summarizer: OnDeviceSummarizer) : ViewModel() {

    private val _uiState = MutableStateFlow(SummaryUiState())
    val uiState = _uiState.asStateFlow()

    init {
        viewModelScope.launch { _uiState.update { it.copy(model = summarizer.status()) } }
    }

    fun downloadModel() {
        viewModelScope.launch {
            summarizer.download()
                .catch { _uiState.update { s -> s.copy(error = "Download failed. Check storage and connection.") } }
                .collect { state -> _uiState.update { it.copy(model = state) } }
        }
    }

    fun summarize(note: String) {
        // The API documents input limits (roughly 4,000 tokens); very short inputs give poor summaries.
        if (note.length < 400) {
            _uiState.update { it.copy(error = "Write a little more before summarising.") }
            return
        }
        viewModelScope.launch {
            _uiState.update { it.copy(isSummarizing = true, summary = "", error = null) }
            try {
                val final = summarizer.summarize(note) { partial ->
                    _uiState.update { it.copy(summary = it.summary + partial) }
                }
                _uiState.update { it.copy(isSummarizing = false, summary = final) }
            } catch (e: GenAiException) {
                _uiState.update { it.copy(isSummarizing = false, error = "Couldn't summarise right now. Please try again.") }
            }
        }
    }

    override fun onCleared() = summarizer.close()
}

In the Compose screen, show the summarise button only when model is Ready, a "Download (one-time)" button with a progress bar for NeedsDownload and Downloading, and nothing at all, or a cloud option if the user has consented, for Unavailable.

The limits that shape your design

ConstraintWhat the docs sayDesign response
Device supportSpecific devices from several manufacturers, with plans to expandRuntime checks; fallback or hide the feature
Foreground onlyInference allowed only when your app is the top foreground app; background use is blockedTrigger from user actions, not from WorkManager or services
QuotasPer-app inference quota (BUSY); long-duration battery quotaExponential backoff; avoid per-keystroke calls
Input size (Summarization)Under ~4,000 tokens; article input over 400 charactersValidate length; chunk or truncate long text
Languages (Summarization)English, Japanese and Korean at the time of writingDetect language first, for example with ML Kit Language ID
Device stateNot supported on devices with an unlocked bootloaderHandle as UNAVAILABLE
Output variationDifferent Gemini Nano versions may return different outputDon't depend on exact wording; test on several devices

When to use the Prompt API instead

The task-specific APIs are the easiest and most predictable option. Choose the Prompt API when your task doesn't match them, for example classifying a support message into categories, extracting a date and amount from a text, or generating a short custom label. Keep prompts short and specific, test across devices, and remember that Prompt API device support is grouped by Gemini Nano version. Official sample code lives in the ML Kit GenAI samples repository.

Real-world use cases

Meeting notes summary

Problem
Long notes, sensitive content, often offline.
Solution
Summarization API, three bullets.
Why
Private, free per request, works without signal.
Trade-off
Supported devices and languages only.

Polish before sending

Problem
Typos in quick messages.
Solution
Proofreading API on demand, shown as a suggestion to accept.
Why
Private messages never leave the phone.
Trade-off
Short text only; user must review.

Alt text for gallery images

Problem
Images lack accessible descriptions.
Solution
Image Description API generates a draft the user can edit.
Why
Personal photos stay local.
Trade-off
Short, generic descriptions; foreground only.

Troubleshooting

  • Always UNAVAILABLE on your test device. Check the published device list, update Google Play services and AICore, and make sure the bootloader isn't unlocked. Emulators generally won't have Gemini Nano.
  • Download never completes. Model downloads can be large and may wait for suitable network conditions. Show clear progress and let users continue using the app.
  • BACKGROUND_USE_BLOCKED means the call happened while your app wasn't the top foreground app. Only trigger inference from visible UI.
  • BUSY means you hit the per-app quota. Retry with exponential backoff and reduce request frequency.
  • Poor summaries of short text. The model performs best on longer input (the docs suggest 300+ words for articles), so don't offer summaries for tiny notes.

Frequently asked questions

Does Gemini Nano increase my APK size?

No. AICore downloads and manages the model at the system level, and your app only includes the ML Kit client library.

Can I fine-tune Gemini Nano or bring my own model to AICore?

The ML Kit GenAI APIs expose Google's managed model. To run your own model on-device, use LiteRT and the related Google AI Edge tooling instead.

Can I run on-device GenAI in a background job?

No. The documentation states inference is only permitted when your app is the top foreground app, and background use, including from a foreground service, is blocked.

How do I support users on unsupported devices?

Either hide the feature or offer a cloud path with clear consent. Firebase AI Logic's experimental hybrid inference can route between on-device and cloud automatically for single-turn requests.

Conclusion

Gemini Nano with the ML Kit GenAI APIs gives Android apps private, offline, no-per-request-cost generative features, managed by the system rather than bundled in your APK. The price is limited device reach, beta APIs, foreground-only execution and quotas, so design every feature with a status check and a fallback.

Next step: add the OnDeviceSummarizer wrapper to a notes screen and handle all four feature states. Then combine it with the cloud path from Gemini API Integration in Android, following the patterns in Cloud AI vs On-Device AI.

Official references

๐Ÿ“ฑ Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps โ†’

Ready to build real Android applications?

Every AI feature still runs inside an Android app that has to be fast, reliable and well structured. Strengthen the Kotlin, Jetpack Compose and architecture skills that AI integrations depend on, then apply these AI concepts with confidence.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp