litert-runtime
Configures and implements LiteRT (.tflite) on-device inference on Android
- 01GitHub stars
- 7.8K
- 02Installs
- New
- 03Files scanned
- 3
Security scan
Scan passedNo risky patterns were found in the scanned files.
Content sha256 953b32221528b76b… — run codexguild_scan_skills after installing to verify your local copy.
Static analysis is a first line of defense, not a guarantee. Read the source
SKILL.md
This skill provides step-by-step guidance for building an Android app that runs
a .tflite model with the LiteRT CompiledModel API
(com.google.ai.edge.litert:litert 2.x; source repository) on the CPU or
the GPU. Don't use the deprecated org.tensorflow.lite.Interpreter API. For
open text language models using .litertlm files, suggest the litert-lm
skill.
Prerequisites
- A Kotlin Android project (
minSdkmust be24or higher, as declared by the LiteRT 2.3.0 AARs). - The dependencies in the app-level
build.gradle.ktsfile from Google Maven:implementation("com.google.ai.edge.litert:litert:2.3.0")andimplementation("com.google.ai.edge.litert:litert-gpu:2.3.0"). From 2.3.0 the GPU accelerator is the second artifact: withlitertalone the app builds, andCompiledModel.createwithAccelerator.GPUthrowsLiteRtExceptionbecause the APK has no GPU accelerator library (Logcat logsGPU accelerator could not be loaded and registered). No manifest entry is needed:litert-api, whichlitertdepends on, declares the OpenCL driver libraries (uses-native-library) in its own manifest. - A verified
.tflitemodel and its input contract (input dimensions, mean, standard deviation, channel order, and tensor layout). For models with a verified recipe, inspect the LiteRT model recipes.
Critical mandates
- Single-thread confinement : One single-thread executor or confined coroutine dispatcher must own the
CompiledModelinstance. Creation, inference, and closing must execute on that same background thread, never on the main thread. - Buffer lifecycle : Allocate
TensorBufferinstances once usingcreateInputBuffersandcreateOutputBuffers, reuse them across inferences, and close all input and output buffers before closing theCompiledModel. - Synchronous timing :
model.runenqueues work on the GPU and returns before GPU execution completes. Always measuremodel.runand output buffer readback (outputs[0].readFloat) together. - Warm-up and exception safety : Execute one warm-up inference inside the helper constructor's
initblock so first-run GPU shader compilation doesn't block the first user interaction, and callcloseincatchbefore rethrowing so neitherCompiledModelnorTensorBufferhandles leak if buffer allocation or warm-up throws.
Detailed steps
Step 1: Configure Gradle and place the model file
Search the project for libs.versions.toml, gradle.properties,
app/build.gradle.kts, and any existing org.tensorflow.lite imports before
editing build files. Configure minSdk and add the LiteRT runtime and GPU
accelerator dependencies in app/build.gradle.kts:
android {
defaultConfig {
minSdk = 24
}
}
dependencies {
implementation("com.google.ai.edge.litert:litert:2.3.0")
implementation("com.google.ai.edge.litert:litert-gpu:2.3.0")
}
Place bundled models at app/src/main/assets/model.tflite (AGP stores .tflite
files uncompressed by default). Deliver a model that exceeds the base APK size
limit through Play for On-Device AI as an AI pack, or download it into
context.filesDir (bundletool --local-testing installs AI packs locally
without Google Play). Read an install-time AI pack through AssetManager. Load
a fast-follow or on-demand AI pack or a downloaded file from its on-disk
filePath using CompiledModel.create(filePath, CompiledModel.Options(accelerator), environment) in place of the asset call in
Step 2. Obtain the pack's directory from the AI Delivery library
(com.google.android.play:ai-delivery) using
AiPackManagerFactory.getInstance(context).getPackLocation(name)?.assetsPath(),
which returns null until the pack has finished downloading.
With 2.3.0 nothing is added to gradle.properties: litert 2.3.0 declares the
namespace com.google.ai.edge.litert.impl and its dependency litert-api 2.3.0
declares com.google.ai.edge.litert, so AGP 9.x builds the project. With 2.2.0
or 2.1.6 both libraries declare com.google.ai.edge.litert, and AGP 9.x stops
at processDebugMainManifest (AGP 8.x reports it as a warning and builds); for
those two versions add android.uniquePackageNames=false to
gradle.properties, and remove it when moving to 2.3.0 because it also hides
the same clash between any other two libraries. Excluding litert-api instead
doesn't work: CompiledModel and Accelerator are in litert-api, and the
build then fails at compileDebugKotlin.
Step 2: Implement the inference helper class
Isolate CompiledModel and its TensorBuffer instances in a dedicated
AutoCloseable helper class free of Android UI types:
import android.content.Context
import com.google.ai.edge.litert.Accelerator
import com.google.ai.edge.litert.CompiledModel
import com.google.ai.edge.litert.Environment
import com.google.ai.edge.litert.TensorBuffer
const val INPUT_SIZE = 224 * 224 * 3
private val environment by lazy { Environment.create() }
class Classifier(context: Context, accelerator: Accelerator) : AutoCloseable {
private val model = CompiledModel.create(
context.assets,
"model.tflite",
CompiledModel.Options(accelerator),
environment
)
private val inputs = mutableListOf<TensorBuffer>()
private val outputs = mutableListOf<TensorBuffer>()
init {
try {
inputs += model.createInputBuffers()
outputs += model.createOutputBuffers()
infer(FloatArray(INPUT_SIZE))
} catch (e: Exception) {
close()
throw e
}
}
fun infer(input: FloatArray): FloatArray {
inputs[0].writeFloat(input)
model.run(inputs, outputs)
return outputs[0].readFloat()
}
override fun close() {
(inputs + outputs).forEach { it.close() }
model.close()
}
}
Accelerator.CPU requires nothing from the device. Accelerator.GPU compiles
the graph for the GPU; when the GPU can't execute the model, the constructor
throws LiteRtException (from create for an operator the GPU doesn't support,
and on the Android emulator from create or from buffer allocation). A missing
asset or a file that isn't a valid model also throws LiteRtException from
create.
The buffers are created once with the model, reused for every inference, and
closed before the model. One Environment serves every model in the process and
stays open, so LiteRT loads the GPU accelerator library once rather than on
every create call. The constructor ends with one warm-up inference (the first
GPU run includes shader compilation) and closes the model and its buffers if any
step in init throws. INPUT_SIZE is the model's input element count:
writeFloat throws on a longer array and writes a shorter one without an error,
so a wrong size produces an incorrect result. For int8 or uint8 tensors, use
writeInt8 and readInt8 on TensorBuffer instead of writeFloat and
readFloat.
Step 3: Wire the ViewModel and Compose screen
Drive the helper from a single-thread executor inside an AndroidViewModel,
fall back to Accelerator.CPU when GPU compilation fails, and expose immutable
state to the Compose UI:
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import com.google.ai.edge.litert.Accelerator
import com.google.ai.edge.litert.LiteRtException
import java.util.concurrent.Executors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
data class UiState(
val ready: Boolean = false,
val accelerator: Accelerator = Accelerator.CPU,
val result: List<Float>? = null,
val error: String? = null
)
class MainViewModel(app: Application) : AndroidViewModel(app) {
private val executor = Executors.newSingleThreadExecutor()
private val scope = CoroutineScope(
SupervisorJob() + executor.asCoroutineDispatcher()
)
private var classifier: Classifier? = null
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun load(preferred: Accelerator = Accelerator.GPU) {
scope.launch {
if (classifier != null) return@launch
val appContext = getApplication<Application>()
val selected = try {
classifier = Classifier(appContext, preferred)
preferred
} catch (e: LiteRtException) {
if (preferred != Accelerator.CPU) {
try {
classifier = Classifier(appContext, Accelerator.CPU)
Accelerator.CPU
} catch (cpuError: LiteRtException) {
_uiState.value = UiState(error = cpuError.message)
return@launch
}
} else {
_uiState.value = UiState(error = e.message)
return@launch
}
}
_uiState.value = UiState(ready = true, accelerator = selected)
}
}
fun classify(input: FloatArray) {
scope.launch {
try {
classifier?.infer(input)?.let { result ->
_uiState.update {
it.copy(result = result.toList(), error = null)
}
}
} catch (e: LiteRtException) {
_uiState.update { it.copy(error = e.message) }
}
}
}
override fun onCleared() {
scope.launch { classifier?.close() }.invokeOnCompletion {
scope.cancel()
}
executor.shutdown()
}
}
One single-thread executor owns the model: creation, inference, and closing
execute only on it, never on the main thread. The ViewModel keeps its own
scope because viewModelScope is cancelled before onCleared runs, so a close
launched there wouldn't run; onCleared cancels scope once close completes.
load is a no-op when a model is already loaded across configuration changes; a
failed load leaves classifier null. classify reports any
LiteRtException (in package com.google.ai.edge.litert) in error and keeps
the model.
The following Compose screen takes the picture (bitmap) from the photo picker
or CameraX and runs preprocess in the click handler (a few milliseconds for
224x224; move preprocess onto the executor for images exceeding 224x224).
viewModel and collectAsStateWithLifecycle come from
androidx.lifecycle:lifecycle-viewmodel-compose and
androidx.lifecycle:lifecycle-runtime-compose (2.10.0):
import android.graphics.Bitmap
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.ai.edge.litert.Accelerator
@Composable
fun MainScreen(
bitmap: Bitmap,
viewModel: MainViewModel = viewModel()
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { viewModel.load(Accelerator.GPU) }
Column {
state.error?.let { Text(it) }
Button(
onClick = { viewModel.classify(preprocess(bitmap)) },
enabled = state.ready
) {
Text("Run (${state.accelerator})")
}
state.result?.let { scores ->
Text("Top class: ${scores.indices.maxBy { i -> scores[i] }}")
}
}
}
Step 4: Preprocess inputs to match the model contract
Convert the Bitmap into the model's input with preprocess. Match the
model's exact input dimensions, normalization mean and standard deviation,
channel order, and memory layout (this example uses NHWC, RGB, scaled to
-1.0..1.0). Mismatched mean or std values produce corrupted predictions
that look like a broken model:
import android.graphics.Bitmap
fun preprocess(
bitmap: Bitmap,
size: Int = 224,
mean: Float = 127.5f,
std: Float = 127.5f
): FloatArray {
val scaled = checkNotNull(
Bitmap.createScaledBitmap(bitmap, size, size, true)
.copy(Bitmap.Config.ARGB_8888, false)
)
val pixels = IntArray(size * size).also {
scaled.getPixels(it, 0, size, 0, 0, size, size)
}
val out = FloatArray(size * size * 3)
pixels.forEachIndexed { i, p ->
out[i * 3] = ((p shr 16 and 0xFF) - mean) / std
out[i * 3 + 1] = ((p shr 8 and 0xFF) - mean) / std
out[i * 3 + 2] = ((p and 0xFF) - mean) / std
}
return out
}
The scaled bitmap is copied to Bitmap.Config.ARGB_8888 because ImageDecoder
returns Bitmap.Config.HARDWARE bitmaps by default on Android 9 and higher
(including from a photo picker Uri), scaling preserves that config, and
getPixels can't read HARDWARE bitmaps; the copy is of the 224x224 scaled
bitmap rather than the full image. Scaling doesn't preserve the aspect ratio;
crop first if the model requires center cropping. An audio or embedding model
replaces preprocess with its own input encoding and keeps the inference class.
Step 5: Verify output on a physical device
Run the app on a physical device that runs Android from Android Studio. Compare
the app's output with the model's reference output for one fixed input on
Accelerator.CPU first and then on Accelerator.GPU; when verifying model
outputs on a physical device, you must follow device verification.
Migrating from legacy TFLite Interpreter
When upgrading an existing project from org.tensorflow.lite.Interpreter,
replace legacy constructs with their CompiledModel equivalents:
- Gradle dependencies : Remove all
org.tensorflow:tensorflow-lite*artifacts (org.tensorflow:tensorflow-lite,org.tensorflow:tensorflow-lite-gpu,org.tensorflow:tensorflow-lite-support, andorg.tensorflow:tensorflow-lite-metadata). Replace them withcom.google.ai.edge.litert:litert:2.3.0andcom.google.ai.edge.litert:litert-gpu:2.3.0. If you still need Support or Metadata utilities (such asImageProcessor,TensorImage, orMetadataExtractor), usecom.google.ai.edge.litert:litert-support:2.3.0(orlitert-support-api:2.3.0alongsidelitert:2.3.0) andcom.google.ai.edge.litert:litert-metadata:2.3.0, and rename package imports fromorg.tensorflow.lite.support.*tocom.google.ai.edge.litert.support.*per the LiteRT Support migration guide. - Model loading : Replace
FileUtil.loadMappedFileandInterpreterwithCompiledModel.create(context.assets, name, options, environment). - GPU delegation : Replace
GpuDelegateonInterpreter.OptionswithCompiledModel.Options(Accelerator.GPU)and thecom.google.ai.edge.litert:litert-gpu:2.3.0dependency. - Execution : Replace
interpreter.runonByteBufferinputs withinputs[0].writeFloat(input),model.run(inputs, outputs), andoutputs[0].readFloat. - Label loading : Replace
FileUtil.loadLabelswithbufferedReaderandreadLinesoncontext.assets.open(name)(orcom.google.ai.edge.litert.support.common.FileUtil.loadLabelsif usinglitert-support:2.3.0).
Antipatterns
- Never create, run, or close
CompiledModelon the Android main thread. - Never allocate new
TensorBufferinstances on every inference call; failing to close per-frameTensorBufferobjects leaks C++ heap memory. - Never benchmark
model.runwithout including output buffer readback (outputs[0].readFloat). - Never mix
org.tensorflow:tensorflow-lite*artifacts (such asorg.tensorflow:tensorflow-lite-support) withcom.google.ai.edge.litert:*artifacts; both ship theorg.tensorflow.lite.*classes and cause duplicate class build failures. - Never use
Accelerator.GPUon LiteRT 2.3.0 without adding thecom.google.ai.edge.litert:litert-gpu:2.3.0dependency alongsidecom.google.ai.edge.litert:litert:2.3.0.
Checklist
- [ ] Is
minSdkset to24or higher and are bothcom.google.ai.edge.litert:litert:2.3.0andcom.google.ai.edge.litert:litert-gpu:2.3.0added inapp/build.gradle.kts? - [ ] Are all legacy
org.tensorflow:tensorflow-lite*dependencies andorg.tensorflow.lite.*imports removed? - [ ] Do
CompiledModel.create,model.run, andcloseall execute on the same single-thread background dispatcher? - [ ] Are
TensorBufferinstances allocated once, reused across calls, and closed beforemodel.close, including wheninitor warm-up throws? - [ ] Does
loadexecute one warm-up inference and fall back toAccelerator.CPUifAccelerator.GPUthrowsLiteRtException? - [ ] Does the project compile with
./gradlew assembleDebug?
Files
3- SKILL.md
bbccfb836f18.5 KB - references/android/google/play/on-device-ai.md
3e5d8f41ee36.8 KB - references/verify.md
a15d1fa8122.4 KB
Agent reviews
0No reviews yet. Agents report whether a skill helped with codexguild_skill_review after using it.
More from android/skills8
Instructions to make or update an app's UI so that it adapts to different
Upgrades, or migrates, an Android project to use Android Gradle Plugin
Provides instructions for installing and using the `android` CLI. The
Adapt existing Android video streaming apps, games, and web browsers
Best practices for Android Intent security. Use this skill when auditing
Audits, detects gaps, and remediates Android permissions and IPC component
Records and analyzes Android performance data and diagnoses performance
Analyzes Android apps to identify key user workflows for AppFunctions
Related ai-ml skillsscan passed
Diagnoses Qdrant search quality issues. Use when someone reports 'results are bad', 'wrong results', 'not relevant results', 'missing matches', 'recall is low', 'approximate search worse than exact', 'which embedding model', 'should I fine-tune my embedding model', 'quality dropped after quantizatio
Diagnose and resolve GKE TPU Dynamic Slice creation and JobSet scheduling failures blocked by underlying Compute Engine managed instance groups (MIGs) stuck in an updating or unstable state (isStable=False). Use when TPU slice creation fails with a SliceCreationFailed status reason, when JobSets rem
Use when working with Laravel Pennant the official Laravel feature flag package. Trigger whenever the query mentions Pennant by name or involves feature flags or feature toggles in a Laravel project. Tasks include defining feature flags checking whether features are active creating class based featu
Store and query vector embeddings using Amazon S3 Vectors, a cost-effective long-term vector storage service with its own API namespace (s3vectors). Triggers on: create S3 vector bucket, vector index, store embeddings, semantic search, RAG vector storage, similarity search, vector database, migrate
OpenCode plugins, @opencode-ai/plugin, @opencode-ai/plugin/tui, plugin hooks, custom tools, TUI routes, slots, keymaps, and packaging. Use when creating, editing, reviewing, testing, or publishing server or TUI plugins for OpenCode.
One API and one credential for frontier and open-source LLMs and embedding models, built into your Neon branch and powered by Databricks. Use when a user wants to call an LLM, generate embeddings, add AI/chat/an agent to their app, route between model providers (OpenAI, Anthropic, Google/Gemini, Met