Skip to main content

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:

VariantFormCarries
Android (arm64-v8a)an AARthe 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 jarthe 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​

RequirementValue
compileSdk36, the level the binding's modules compile against
minSdk28: a device below it installs the AAR but cannot load the runtime library
Android Gradle plugin9.3.2, which needs gradle 9.5 or newer
Kotlin2.3 or newer in the app: the binding's classes are compiled by Kotlin 2.3, and an older compiler refuses their metadata
JDK17
NDKnone: an app that takes the published artifact builds nothing native
ABIarm64-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.