---
title: Package ClikaRT in an Android app
description: "What an Android app ships to run the runtime and the model library on the phone: the one artifact and the libraries inside it, the build requirements, the license credential, the thread count, the cache root, and a server that outlives the screen."
---

{/* CERTIFICATION: every fact on this page is read from the Kotlin binding's README and gradle files of the
     pinned release (compileSdk, minSdk, the plugin versions, the AAR contents, the load entries) and from the
     release's Kotlin artifact itself (its Maven repository directory); no app on this page is compiled by the
     docs build. */}

The [tutorial's last part](../getting-started/first-program/06-deploy-to-mobile.mdx) pushes a
binary to a phone over `adb`. An app does the same thing from inside its own process: it ships
the runtime library, loads it through the Kotlin binding, and places the license credential
before the first call. This page is the inventory of what the app carries and the few facts
that are easy to get wrong the first time.

## The one artifact

One Maven artifact, `io.clika:clika-runtime`, carries the runtime and the model library's Kotlin API
together. It is published as a Maven repository directory inside `clika-runtime-maven-<version>.zip`,
a download of the platform beside the release archives ([Get ClikaRT](../getting-started/get-clikart.mdx));
extract it anywhere and name the directory as a repository, then one dependency line serves both
products:

```kotlin
repositories {
    maven { url = uri("/path/to/clika-runtime-maven-<version>") }
    google()
    mavenCentral()
}

dependencies {
    implementation("io.clika:clika-runtime:<version>")
}
```

The artifact is a Kotlin Multiplatform library with two variants; gradle reads its module metadata and
picks the one the project builds for:

| Variant | Form | Carries |
| --- | --- | --- |
| Android (`arm64-v8a`) | an AAR | the Kotlin classes of both packages (`io.clika.runtime`, `io.clika.modelverse`), the two JNI bridges, every library of the Android release archive (`libClikaRT.so`, its backend images, `libClikaRT_modelverse.so`) under `jni/arm64-v8a/`, the consumer keep rules, and the archive's third-party notices under `META-INF/licenses/` |
| desktop JVM (Linux, Windows, macOS) | a jar | the Kotlin classes of both packages, the two JNI bridges per desktop platform, and the notices; the runtime and model libraries come from the release archive's `lib/` on `java.library.path` |

On Android the AAR carries everything: the app copies nothing into `jniLibs`. At run time the
binding loads `libClikaRT.so` first, then the runtime bridge; `Modelverse.load` loads
`libClikaRT_modelverse.so` and the model library's bridge after it. The binding is built against one
release and refuses to load another, naming both versions (`BINDING_VERSION_MISMATCH`).

## The build requirements

| Requirement | Value |
| --- | --- |
| `compileSdk` | 36, the level the binding's modules compile against |
| `minSdk` | 28: a device below it installs the AAR but cannot load the runtime library |
| Android Gradle plugin | 9.3.2, which needs gradle 9.5 or newer |
| Kotlin | 2.3 or newer in the app: the binding's classes are compiled by Kotlin 2.3, and an older compiler refuses their metadata |
| JDK | 17 |
| NDK | none: an app that takes the published artifact builds nothing native |
| ABI | `arm64-v8a` alone |

The binding reaches nothing by reflection, so an app's R8 pass needs no rule of its own: the AAR
carries the consumer rules for the classes its bridges bind by name.

## Load, and place the credential first

An app has no shell to export a variable from and no per-user license file, so the credential is
an argument of the load. `ClikaRtAndroid.load(context, license = "CLIKA1-...")` places it for
the process and loads the runtime; an app that uses the model library calls
`Modelverse.load(context, license)` instead, which runs the whole sequence, the runtime first.
A credential given there replaces one already in the process environment; the default, `null`,
leaves the environment as it is. The credential's text is never logged. Ship it the way you ship
any other secret the app needs at start-up: a build-time field read from a file outside the
source tree, or a value the app asks for once and keeps in its own secret store. Without a valid
credential the first operator throws a `ClikaRtException` (a `ModelverseException` from the
model library) whose `codeName` is `LICENSE_FAILED`, or `LICENSE_EXPIRED` past the end date.

Two more facts are placed before the first call. The CPU worker count, `CLIKA_RT_NUM_THREADS`,
is read once before the first compute; an app sets it in its own process with
`Os.setenv("CLIKA_RT_NUM_THREADS", "<count>", true)` before the load. A phone with a few large
cores and several small ones runs best at the large cores' count; measure on the device. The cache
root is the app's own cache directory: `ClikaRtAndroid.load` sets `XDG_CACHE_HOME` to
`Context.getCacheDir()` unless the app set it itself, and the hub cache the model library
downloads into sits under it in the Hugging Face layout, so a snapshot downloaded on another
machine and copied there serves with no network.

Every call into the runtime runs on a thread of the app's own, never on the main thread: a model
load blocks for the load, and `generate` blocks for the reply. After the load,
`ClikaRtAndroid.trimOnMemoryPressure(context)` lets the runtime give back its cached memory when
the platform reports pressure, and `ClikaRtAndroid.bridgeLogs()` routes the runtime's log lines
to logcat.

## A model on the phone

`AutoModelForCausalLM.fromPretrained(source, LoadOptions(contextLength = 4096, cacheDir = ...))`
takes a hub repository id (the snapshot downloads inside the load), a snapshot directory, or a
`.gguf` file. Set the context length on a phone: the key-value cache is sized for the window, and
a checkpoint's own window is often tens of thousands of tokens. `chat(messages, config, listener)`
returns at once and calls the listener on the model's worker thread, `onToken` per visible piece
and `onDone` with the report; the handle's `cancel()` stops the decode at its next token.
`generate(prompt = ...)` is the blocking form. `Modelverse.registry()` lists the families the
library serves with the keys a checkpoint is matched by, so an app checks a model before it
downloads one. The whole surface, kind by kind, is the binding's README.

## A server that outlives the screen

`Modelverse.serve(model, ServeOptions(port = 8129))` hosts the library's chat API on the phone
(the OpenAI chat route, the model list and the chat page when enabled), on the server's own
threads; `start()` returns at once and `baseUrl` is the address another device on the network
points its client at. A server started
from an `adb shell` dies with the shell and with the phone's next reboot; an app keeps it alive
by hosting it in a foreground service with an ongoing notification (the connected-device service
type, which Android 14 and newer admits with a network-class permission beside it), so the
process survives the activity leaving the screen and starts again with the app.

## Where the runnable proof is

The examples archive ([Additional examples](../examples.md)) carries the Android programs. The Kotlin
chapters under its `kotlin/clika_rt/` directory are instrumented tests that run the same calls on a
connected device (`gradle connectedAndroidTest -PclikaRtMavenRepo=<dir>`): the operator surface
through `Ops`, and the device and backend facts through `Backends`. Its `android/` directory holds
three sample apps over the same artifact, one screen each: `hello` loads the runtime and prints the
version, the backends with their devices and one operator's result; `chat` loads a chat model and
streams its replies; `serve` hosts a loaded model for the other devices on the network from a
foreground service. Every one of them takes the artifact through the `clikaRtMavenRepo` property and
copies nothing into `jniLibs`.
