Package ClikaRT in an Android app
The tutorial's last part 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);
extract it anywhere and name the directory as a repository, then one dependency line serves both
products:
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) 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.