Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Gemma Android Template

A production-ready Android template that runs Gemma 4 fully on-device — no internet required after first setup. Drop it into Claude Code and describe what you want to build on top.

What this is

This template handles everything below the UI layer: model download, on-device LLM inference via LiteRT-LM, offline enforcement, and device capability detection. The demo screen is a minimal chat prompt box. Replace it with whatever you're building.

Package: com.gemma.template
Min SDK: 26 (Android 8.0)
LLM library: com.google.ai.edge.litertlm:litertlm-android:0.10.2
Default model: Gemma 4 E2B (~2 GB) — falls back to Gemma 3 1B (~555 MB) on lower-RAM devices
Model format: .litertlm (LiteRT-LM — not the older MediaPipe .task format)


Architecture

app/src/main/kotlin/com/gemma/template/
├── GemmaApp.kt                        # @HiltAndroidApp — wires WorkManager + Hilt
├── MainActivity.kt                    # Single-activity host, sets AppTheme + AppNavGraph
│
├── llm/
│   ├── LlmEngine.kt                   # Interface: initialize(file) / generate(prompt) / close()
│   ├── LiteRtLmEngine.kt              # Real inference — LiteRT-LM Engine, one Conversation per call
│   └── MockLlmEngine.kt              # Debug fake — streams canned text at 30ms/word
│
├── data/model/
│   └── ModelRepository.kt             # Download, file resolution, SHA-256 verify, tier prefs
│
├── download/
│   └── DownloadModelWorker.kt         # WorkManager HiltWorker — background download + notification
│
├── di/
│   └── AppModule.kt                   # Hilt: binds LlmEngine → Mock (debug) or LiteRtLm (share/release)
│
├── util/
│   ├── DeviceCapabilityDetector.kt    # Reads RAM/storage/ABI → recommends ModelTier
│   ├── ModelTier.kt                   # Enum: GEMMA4_E2B / GEMMA3_1B / UNSUPPORTED
│   └── NetworkGate.kt                 # Permanently revokes internet after model is verified
│
└── ui/
    ├── AppNavGraph.kt                 # Routes: DOWNLOAD → CHAT (replace CHAT with your screen)
    ├── theme/AppTheme.kt              # Material 3, dynamic colour on Android 12+
    ├── download/
    │   ├── DownloadScreen.kt          # Device info card, model picker, progress, celebration
    │   └── DownloadViewModel.kt       # States: Loading → ReadyToDownload → Downloading → Ready
    └── chat/                          ← REPLACE THIS WITH YOUR OWN UI
        ├── ChatScreen.kt              # Demo: message list + streaming input bar
        └── ChatViewModel.kt           # Loads model on init, exposes send(prompt), streaming state

Layer responsibilities

LlmEngine (interface)

interface LlmEngine {
    suspend fun initialize(modelFile: File)   // call once before generate
    suspend fun generate(prompt: String): Flow<String>  // streams tokens
    fun close()
}

Both LiteRtLmEngine and MockLlmEngine implement this. Hilt injects the right one at build time based on BuildConfig.USE_MOCK_LLM. Your ViewModel only ever sees LlmEngine.

ModelRepository

Owns model file resolution and download. Lookup order:

  1. context.filesDir/models/<filename> — internal storage (primary)
  2. /sdcard/Android/data/com.gemma.template/files/models/ — push via adb without root
  3. /sdcard/Download/<filename> — dev/emulator fast-path

Download uses 8 parallel HTTP range requests for speed, verifies SHA-256 on completion, and can resume interrupted downloads. Model URLs and filenames live in a modelConfigs map — swap them to point at a different model.

DeviceCapabilityDetector + ModelTier

Reads total RAM and free storage at runtime. Recommends:

RAM Free storage Tier
≥ 4 GB ≥ 2.5 GB GEMMA4_E2B
≥ 2 GB ≥ 1 GB GEMMA3_1B
below either UNSUPPORTED

NetworkGate

After the model is downloaded and verified, revoke() is called once. It sets a SharedPreferences flag and the app never opens a network connection again. This is how the offline guarantee is enforced. The gate can be reset in debug builds for re-testing the download flow.

DownloadViewModel states

Loading → ReadyToDownload → Downloading → Celebrating → Ready (navigates away)
                                      ↘ Error → (retry) → Loading
Loading → Unsupported (device below minimum specs)

Ready state triggers navigation to the next screen. In debug builds (USE_MOCK_LLM=true) the ViewModel short-circuits straight to Ready — no download needed.


Build variants

Variant USE_MOCK_LLM Model Purpose
debug true None (mock) Fast iteration — no download, no model file
share false Real Gemma Sideloadable — debug-signed, anyone can install
release false Real Gemma Play Store / production
# Requires Java 17
export JAVA_HOME=/opt/homebrew/Cellar/openjdk@17/17.0.18/libexec/openjdk.jdk/Contents/Home

# Debug build (mock LLM, instant, for emulator)
./gradlew assembleDebug

# Share build (real Gemma, for sideloading on real device or capable emulator)
./gradlew assembleShare

# Install on connected device/emulator
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb install -r app/build/outputs/apk/share/app-share.apk

If your model requires a HuggingFace token (gated repo), add it to local.properties:

hf_token=hf_yourTokenHere

Testing on the emulator

The debug build works on any emulator — mock LLM, instant response, no model file needed.

For real inference (share build), the emulator must be:

  • ARM64 (arm64-v8a ABI) — LiteRT-LM is not x86 compatible
  • ≥ 4 GB RAM configured in the AVD

On Apple Silicon Macs, ARM64 emulators run natively. To create a capable AVD:

Android Studio → Device Manager → Create → Pixel 8 Pro → ARM64 system image → RAM: 6144 MB

Alternatively, push a model file directly to the emulator instead of downloading:

# Push model to the adb-accessible fast-path
adb push gemma-4-E2B-it.litertlm /sdcard/Download/

# The app will detect and copy it to internal storage on next launch

How to extend this template

1. Replace the chat screen

Delete ui/chat/ChatScreen.kt and ChatViewModel.kt. Create your own screen. The only contract is:

@HiltViewModel
class YourViewModel @Inject constructor(
    private val llmEngine: LlmEngine,
    private val modelRepository: ModelRepository
) : ViewModel() {

    init {
        viewModelScope.launch {
            if (!BuildConfig.USE_MOCK_LLM) {
                val file = modelRepository.getModelFile(modelRepository.getChosenTier())!!
                llmEngine.initialize(file)
            }
        }
    }

    fun generate(prompt: String): Flow<String> = flow {
        llmEngine.generate(prompt).collect { emit(it) }
    }
}

2. Add a route

In AppNavGraph.kt:

object Routes {
    const val DOWNLOAD = "download"
    const val CHAT = "chat"
    const val YOUR_SCREEN = "your_screen"  // add this
}

// In NavHost:
composable(Routes.YOUR_SCREEN) {
    YourScreen()
}

Change the onModelReady navigation target in the DOWNLOAD composable to point at YOUR_SCREEN.

3. Swap or add a model

In ModelRepository.kt, find modelConfigs and add or replace an entry:

ModelTier.GEMMA4_E2B to ModelConfig(
    filename = "your-model-filename.litertlm",
    downloadUrl = "https://huggingface.co/your-org/your-repo/resolve/main/your-model.litertlm",
    isGated = false,          // set true if repo requires HuggingFace login
    knownSha256 = "TODO_POPULATE_AFTER_FIRST_DOWNLOAD"
)

After the first successful download the app logs the real SHA-256 — copy it into knownSha256 for integrity checking on subsequent installs.

4. Add persistent storage

The template has no database. To add Room:

// app/build.gradle.kts — add:
implementation(libs.room.runtime)
implementation(libs.room.ktx)
ksp(libs.room.compiler)

Define your @Entity, @Dao, and @Database classes. Wire them in AppModule.kt the same way as the rest of the Hilt bindings.

5. Add speech input / output

The template has no STT or TTS. To add Android's built-in speech recogniser:

val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
    putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
}
startActivityForResult(intent, REQUEST_CODE_SPEECH)

For TTS, android.speech.tts.TextToSpeech works offline with the device's built-in engine. Add android.permission.RECORD_AUDIO to the manifest for STT.


Key technical facts

  • LiteRT-LM API: Engine(EngineConfig(modelPath))engine.initialize()engine.createConversation()conversation.sendMessageAsync(prompt) collects Flow<Message> — each message contains Content.Text parts
  • One Conversation per call: a new Conversation is created for each generate() call so context does not bleed between turns. To support multi-turn memory, hold the Conversation open across calls in your ViewModel
  • Thread safety: LiteRtLmEngine.initialize() and generate() both run on Dispatchers.IO. Do not call them from the main thread
  • Cold-load time: ~10–30 seconds for the first initialize() call on device (555 MB–2 GB model read into memory)
  • Model file integrity: files starting with <html or HTTP/ are rejected as CDN error pages before they can crash the LLM loader
  • Offline after setup: NetworkGate.revoke() is called once the model passes the SHA-256 check. After that, the app makes zero network calls

Prompt to give Claude Code when building on this template

Paste this at the start of a new session:

I'm building [describe your app] on top of the Gemma Android template.

Architecture:
- LlmEngine interface: initialize(File), generate(String): Flow<String>, close()
- Hilt injects MockLlmEngine (debug) or LiteRtLmEngine (share/release) automatically
- ModelRepository handles model download and file resolution
- DownloadScreen/DownloadViewModel handles first-run setup — do not modify
- AppNavGraph: DOWNLOAD → CHAT (replace CHAT with my screen)
- Package: com.gemma.template
- Build: debug = mock LLM, share = real Gemma 4 E2B on-device

What I want to build: [describe your use case]

Start by replacing ui/chat/ with the screens I need.

About

Production-ready Android template for running Gemma 4 fully on-device via LiteRT-LM. No internet required after first setup.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages