| name | sdk-kotlin |
| description | Build, generate, and publish the Onde Inference Kotlin Multiplatform SDK (Android + JVM). Covers UniFFI Kotlin binding generation, KMP source set layout with shared srcDir pattern, cargo-ndk cross-compilation for Android ABIs, JVM native library bundling for macOS Apple Silicon, Maven Central publishing via Vanniktech plugin, klibs.io discovery requirements, ProGuard consumer rules, and the Jetpack Compose example app. |
| allowed-tools | Read, Write, Edit, Glob, Grep, Bash |
| user-invocable | true |
Skill: Kotlin Multiplatform SDK
What This Skill Covers
Building the onde-inference Kotlin Multiplatform library from Rust source
using UniFFI, distributing it via Maven Central, and getting it listed on
klibs.io — the JetBrains-maintained discovery platform for
KMP libraries.
Targets:
| Target | GPU Backend | Default Model | Primary Use Case |
|---|
androidTarget() | CPU (Candle) | Qwen 2.5 1.5B (~941 MB) | Mobile apps |
jvm() | Metal (macOS) / CPU (Linux) | Qwen 2.5 3B (~1.93 GB) | Desktop / server on Apple Silicon |
Context: Onde's tagline is "AI for Apple silicon devices." The primary SDKs
are Swift (iOS, tvOS, macOS) and Dart (Flutter, cross-platform). The
Kotlin SDK extends Onde to Android and JVM developers — particularly those
building KMP apps that target both Android and macOS, or running inference on
Apple Silicon Macs from Kotlin/JVM.
Repository Layout
onde/
├── src/ # Rust source — UniFFI-annotated types and OndeChatEngine
├── uniffi-bindgen/ # Standalone uniffi-bindgen CLI binary (pinned =0.31.0)
├── sdk/kotlin/
│ ├── settings.gradle.kts # Gradle root — includes :lib and :example
│ ├── build.gradle.kts # root — plugin declarations (all versions live here)
│ ├── gradle.properties # GROUP, POM_ARTIFACT_ID, VERSION_NAME, signing/OSSRH
│ ├── gradle/wrapper/
│ │ └── gradle-wrapper.properties # pinned Gradle 8.9
│ ├── lib/
│ │ ├── build.gradle.kts # KMP library + Vanniktech maven-publish
│ │ ├── consumer-rules.pro # ProGuard: preserve UniFFI JNI bridge symbols
│ │ └── src/
│ │ ├── commonMain/kotlin/.gitkeep # empty — KMP requires this source set to exist
│ │ ├── shared/kotlin/ # shared srcDir added to BOTH androidMain and jvmMain
│ │ │ └── com/ondeinference/onde/
│ │ │ ├── PlatformSupport.kt # internal interface — setEnv + ensureNativeLoaded
│ │ │ ├── OndeInference.kt # engine wrapper (internal constructor)
│ │ │ └── Convenience.kt # OndeSampling, OndeModels, OndeMessage
│ │ ├── generated/kotlin/ # UniFFI-generated onde.kt ← GITIGNORED
│ │ ├── androidMain/
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── jniLibs/ # compiled .so per ABI ← GITIGNORED
│ │ │ └── kotlin/com/ondeinference/onde/
│ │ │ └── Platform.android.kt # AndroidPlatform + factory fun OndeInference(context)
│ │ └── jvmMain/
│ │ ├── kotlin/com/ondeinference/onde/
│ │ │ ├── Platform.jvm.kt # JvmPlatform + factory fun OndeInference(dataDir)
│ │ │ └── NativeLoader.kt # extract libonde from JAR resources
│ │ └── resources/native/ # bundled .dylib/.so ← GITIGNORED
│ │ ├── macos-aarch64/libonde.dylib
│ │ ├── macos-x86_64/libonde.dylib
│ │ ├── linux-x86_64/libonde.so
│ │ └── linux-aarch64/libonde.so
│ ├── example/
│ │ ├── build.gradle.kts # com.android.application — no plugin versions here
│ │ ├── proguard-rules.pro
│ │ ├── README.md
│ │ └── src/main/
│ │ ├── AndroidManifest.xml # INTERNET permission
│ │ ├── res/values/
│ │ │ ├── strings.xml
│ │ │ └── themes.xml # Material3 NoActionBar activity theme
│ │ └── kotlin/com/ondeinference/example/
│ │ ├── MainActivity.kt # ComponentActivity — enableEdgeToEdge + setContent
│ │ ├── ChatViewModel.kt # model lifecycle, streaming, history
│ │ └── ui/
│ │ ├── ChatScreen.kt # full Compose UI
│ │ └── theme/Theme.kt # dynamic colour + dark mode
│ └── scripts/
│ ├── build-android.sh # Rust .so → androidMain/jniLibs/ via cargo-ndk
│ ├── build-jvm.sh # Rust .dylib/.so → jvmMain/resources/native/
│ └── generate-bindings.sh # uniffi-bindgen → src/generated/kotlin/onde.kt
└── .github/workflows/
└── release-sdk-kotlin.yml # tag push → build → Maven Central publish
KMP Architecture
Why KMP Instead of Android-Only
- klibs.io listing — the JetBrains KMP discovery platform requires
kotlin-tooling-metadata.json, which only the kotlin.multiplatform Gradle
plugin generates. An Android-only AAR will not appear on klibs.io.
- JVM target — Kotlin developers on macOS Apple Silicon can run inference
locally from desktop/server apps, Gradle plugins, or CLI tools.
- Future-proof — adding Kotlin/Native targets (iOS, macOS) later requires
no structural changes. The Swift SDK already covers Apple platforms natively,
but KMP apps that share Kotlin across Android + iOS could benefit.
Source Set Strategy: Shared srcDir
Instead of KMP intermediate source sets (which add Gradle complexity and
require careful dependency resolution for JNA), the SDK uses a shared srcDir
pattern:
commonMain/ — empty (KMP requires it)
shared/ — hand-written wrapper code (compiled for BOTH targets)
generated/ — UniFFI-generated onde.kt (compiled for BOTH targets)
androidMain/ — Android-specific: Os.setenv, Context factory
jvmMain/ — JVM-specific: JNA setenv, NativeLoader, File factory
In lib/build.gradle.kts:
androidMain.get().kotlin.srcDir("src/shared/kotlin")
androidMain.get().kotlin.srcDir("src/generated/kotlin")
jvmMain.get().kotlin.srcDir("src/shared/kotlin")
jvmMain.get().kotlin.srcDir("src/generated/kotlin")
Both targets compile the shared code independently with their own dependencies.
Since both are JVM-based, java.io.File, JNA, and coroutines are available in
the shared code.
Platform Abstraction: Interface Pattern
The shared OndeInference class takes a PlatformSupport interface via an
internal constructor. Each target provides a concrete implementation and a
top-level factory function:
PlatformSupport (interface — shared/)
├── AndroidPlatform (object — androidMain/)
│ └── fun OndeInference(context, dataDir?) → OndeInference
└── JvmPlatform (object — jvmMain/)
└── fun OndeInference(dataDir?) → OndeInference
| Operation | Android | JVM |
|---|
setEnv(key, value) | Os.setenv(key, value, true) | JNA → libc.setenv(key, value, 1) |
ensureNativeLoaded() | No-op (APK jniLibs) | NativeLoader.ensureLoaded() |
Default dataDir | context.filesDir | ~/.onde/ |
Key Design Decisions
| Decision | Rationale |
|---|
KMP with androidTarget() + jvm() | Required for klibs.io listing; enables macOS Apple Silicon JVM users |
| Shared srcDir instead of intermediate source sets | Avoids KMP dependency resolution complexity with JNA (@aar vs JAR) |
| Interface-based platform abstraction | Simpler than expect/actual when constructor signatures differ between targets |
| UniFFI Kotlin bindings — not hand-written JNI | Same Rust source as Swift/Dart, zero API drift |
src/generated/kotlin/ is gitignored | Regenerated from Rust on every release |
cargo-ndk for Android cross-compilation | Handles NDK toolchain wiring for all 4 ABIs transparently |
NativeLoader extracts dylib from JAR resources | JVM users don't need to manage java.library.path manually |
Vanniktech maven-publish plugin with KotlinMultiplatform | Publishes KMP metadata + per-target artifacts to Maven Central |
minSdk 26 | Required for android.system.Os.setenv to set HF_HOME |
JNA dependency (net.java.dev.jna:jna:5.14.0) | UniFFI 0.31.0 generates Kotlin bindings using com.sun.jna.*. Android uses @aar variant; JVM uses plain JAR. |
| ProGuard consumer rules bundled in AAR | Prevents R8 from stripping UniFFI JNI bridge in consumer apps |
:example is a Gradle submodule | Plugin versions declared once in root build.gradle.kts and inherited |
UniFFI Kotlin Type Map
| Rust type | UniFFI derive | Kotlin type |
|---|
ChatRole | uniffi::Enum | sealed class ChatRole |
ChatMessage | uniffi::Record | data class ChatMessage |
SamplingConfig | uniffi::Record | data class SamplingConfig |
GgufModelConfig | uniffi::Record | data class GgufModelConfig |
InferenceResult | uniffi::Record | data class InferenceResult |
StreamChunk | uniffi::Record | data class StreamChunk |
EngineStatus | uniffi::Enum | sealed class EngineStatus |
EngineInfo | uniffi::Record | data class EngineInfo |
InferenceError | uniffi::Error | sealed class InferenceException : Exception |
OndeChatEngine | uniffi::Object | class OndeChatEngine |
StreamChunkListener | callback_interface | interface StreamChunkListener |
The generated Kotlin package is uniffi.onde.*. The shared OndeInference.kt
re-exports these as typealias under com.ondeinference.onde.* so callers only
need one import.
Build Targets
Android ABI Targets
| ABI | Rust triple | Typical device |
|---|
arm64-v8a | aarch64-linux-android | Modern Android phones (primary) |
armeabi-v7a | armv7-linux-androideabi | Older 32-bit ARM phones |
x86_64 | x86_64-linux-android | Android emulators (Intel/AMD) |
x86 | i686-linux-android | 32-bit emulators |
JVM Native Library Targets
| Platform | Rust triple | Library | Primary Use Case |
|---|
| macOS Apple Silicon | aarch64-apple-darwin | libonde.dylib | Primary — Mac development + Metal GPU |
| macOS Intel | x86_64-apple-darwin | libonde.dylib | Legacy Mac support |
| Linux x86_64 | x86_64-unknown-linux-gnu | libonde.so | CI, servers |
| Linux ARM64 | aarch64-unknown-linux-gnu | libonde.so | ARM servers |
Build Sequence (Manual / CI)
1. Prerequisites
rustup toolchain install stable
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android
cargo install cargo-ndk
export ANDROID_NDK_HOME=$HOME/Library/Android/sdk/ndk/26.1.10909125
rustup target add aarch64-apple-darwin
2. Build Android .so files
./sdk/kotlin/scripts/build-android.sh
./sdk/kotlin/scripts/build-android.sh --debug
FILTER_ABI=arm64-v8a ./sdk/kotlin/scripts/build-android.sh
Outputs land in sdk/kotlin/lib/src/androidMain/jniLibs/<ABI>/libonde.so.
3. Build JVM native library
./sdk/kotlin/scripts/build-jvm.sh
./sdk/kotlin/scripts/build-jvm.sh --debug
ONDE_TARGET_TRIPLE=x86_64-apple-darwin ./sdk/kotlin/scripts/build-jvm.sh
Outputs land in sdk/kotlin/lib/src/jvmMain/resources/native/<os-arch>/libonde.<ext>.
4. Build the uniffi-bindgen CLI
cargo build --manifest-path uniffi-bindgen/Cargo.toml --release
5. Generate Kotlin UniFFI bindings
./sdk/kotlin/scripts/generate-bindings.sh
Produces sdk/kotlin/lib/src/generated/kotlin/onde.kt — the complete
generated binding including all types, the OndeChatEngine class, free
functions, and System.loadLibrary("onde") in a companion object static init.
6. Build the library
cd sdk/kotlin
./gradlew :lib:assembleRelease
./gradlew :lib:jvmJar
./gradlew :lib:build
JVM Native Library Loading
The JVM target bundles native libraries inside the JAR under
/native/<os>-<arch>/libonde.<ext>. The NativeLoader object handles
extraction and loading at runtime:
OndeInference() factory (jvmMain)
└── JvmPlatform.ensureNativeLoaded()
└── NativeLoader.ensureLoaded()
├── Detect OS + arch from system properties
├── Extract /native/<os>-<arch>/libonde.<ext> from JAR resources
├── Write to temp file: <java.io.tmpdir>/onde-native/libonde.<ext>
└── System.load(tempFile.absolutePath)
If the bundled library is not found (e.g. development mode without running
build-jvm.sh), NativeLoader falls back to System.loadLibrary("onde")
which searches java.library.path.
Important: NativeLoader.ensureLoaded() must run BEFORE any UniFFI type is
accessed. The OndeInference constructor handles this via the init block:
class OndeInference internal constructor(...) : AutoCloseable {
init { platform.ensureNativeLoaded() }
private val engine = uniffi.onde.OndeChatEngine()
}
Android Filesystem Sandbox Setup
On Android, dirs::home_dir() (used by hf-hub under the hood) panics because
there is no home directory in the Android sandbox. The Rust engine requires
HF_HOME to be set before any model load on Android.
The OndeInference wrapper handles this automatically in setup():
fun setup() {
if (configured) return
val hfHome = File(dataDir, "models").also { it.mkdirs() }
val hfHubCache = File(hfHome, "hub").also { it.mkdirs() }
val tmpDir = File(dataDir, "tmp").also { it.mkdirs() }
platform.setEnv("HF_HOME", hfHome.absolutePath)
platform.setEnv("HF_HUB_CACHE", hfHubCache.absolutePath)
platform.setEnv("HUGGINGFACE_HUB_CACHE", hfHubCache.absolutePath)
platform.setEnv("TMPDIR", tmpDir.absolutePath)
configured = true
}
setup() is called automatically by loadDefaultModel() and loadModel().
Filesystem layout
# Android (context.filesDir)
<filesDir>/
├── models/ ← HF_HOME
│ └── hub/ ← HF_HUB_CACHE
└── tmp/ ← TMPDIR
# JVM (defaults to ~/.onde/)
~/.onde/
├── models/ ← HF_HOME
│ └── hub/ ← HF_HUB_CACHE
└── tmp/ ← TMPDIR
Cross-platform model cache comparison
| Platform | SDK | Cache Location | Shared across apps? |
|---|
| iOS / tvOS / macOS | Swift | App Group container | Yes (same team + entitlement) |
| macOS (JVM) | Kotlin | ~/.onde/models/hub/ | Yes (all JVM apps on same user) |
| Android | Kotlin | <filesDir>/models/hub/ | No (per-app sandbox) |
| Flutter | Dart | Platform-dependent | Follows platform conventions |
Why Android can't share models across apps
Android's security model assigns each app a unique Linux UID at install time.
The kernel enforces file permissions — app A cannot open a file descriptor
inside app B's /data/data/com.B/files/. There is no platform primitive to
override this the way iOS App Groups do. The approaches that don't work:
android:sharedUserId — deprecated API 29, removed API 33
MANAGE_EXTERNAL_STORAGE — Google Play rejects unless file manager
- SAF (Storage Access Framework) — requires user interaction every time
Each Onde-powered Android app downloads its own copy (~941 MB). This matches
how TensorFlow Lite, MediaPipe, and ML Kit handle model caching. The SDK caches
after first download — bandwidth cost is one-time per app.
Kotlin Public API
OndeInference — primary entry point
import com.ondeinference.onde.OndeInference
import com.ondeinference.onde.OndeSampling
import com.ondeinference.onde.OndeModels
import com.ondeinference.onde.OndeMessage
val onde = OndeInference(context)
val onde = OndeInference()
val onde = OndeInference(File("/custom"))
val elapsed: Double = onde.loadDefaultModel(
systemPrompt = "You are helpful.",
sampling = OndeSampling.mobile()
)
val result = onde.chat("What is Rust?")
println(result.text)
onde.stream("Tell me a story.").collect { chunk ->
print(chunk.delta)
}
val summary = onde.generate(
messages = listOf(
OndeMessage.system("Summarise this."),
OndeMessage.user(longText)
)
)
val info = onde.info()
onde.clearHistory()
onde.pushHistory(OndeMessage.user("Injected context"))
onde.unload()
onde.close()
OndeSampling — convenience constructors
OndeSampling.default()
OndeSampling.deterministic()
OndeSampling.mobile()
OndeModels — model config constructors
OndeModels.default()
OndeModels.qwen25_1_5b()
OndeModels.qwen25_3b()
OndeMessage — message constructors
OndeMessage.system("You are helpful.")
OndeMessage.user("Hello!")
OndeMessage.assistant("Hi there!")
lib/build.gradle.kts Key Sections
import com.vanniktech.maven.publish.KotlinMultiplatform
import com.vanniktech.maven.publish.JavadocJar
import com.vanniktech.maven.publish.SonatypeHost
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("com.android.library")
id("com.vanniktech.maven.publish")
}
kotlin {
androidTarget {
publishLibraryVariants("release")
compilations.all { kotlinOptions { jvmTarget = "17" } }
}
jvm {
compilations.all { kotlinOptions { jvmTarget = "17" } }
}
sourceSets {
androidMain.dependencies {
implementation("net.java.dev.jna:jna:5.14.0@aar")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
}
jvmMain.dependencies {
implementation("net.java.dev.jna:jna:5.14.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
}
androidMain.get().kotlin.srcDir("src/shared/kotlin")
androidMain.get().kotlin.srcDir("src/generated/kotlin")
jvmMain.get().kotlin.srcDir("src/shared/kotlin")
jvmMain.get().kotlin.srcDir("src/generated/kotlin")
}
}
mavenPublishing {
configure(KotlinMultiplatform(
javadocJar = JavadocJar.Empty(),
sourcesJar = true,
))
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
signAllPublications()
coordinates("com.ondeinference", "onde-inference", "<version>")
}
The Vanniktech plugin reads signing and OSSRH credentials from environment
variables prefixed with ORG_GRADLE_PROJECT_:
| Gradle property | CI env var |
|---|
mavenCentralUsername | ORG_GRADLE_PROJECT_mavenCentralUsername |
mavenCentralPassword | ORG_GRADLE_PROJECT_mavenCentralPassword |
signingKeyId | ORG_GRADLE_PROJECT_signingKeyId |
signingKey | ORG_GRADLE_PROJECT_signingKey |
signingPassword | ORG_GRADLE_PROJECT_signingPassword |
ProGuard Consumer Rules
The consumer-rules.pro bundled in the AAR instructs R8/ProGuard in consumer
apps to preserve:
uniffi.onde.** — all generated binding classes
com.ondeinference.onde.** — the public wrapper
uniffi.onde.StreamChunkListener — callback interface called from Rust
- Native method declarations (
keepclasseswithmembernames)
- Static initializers in
uniffi.onde.* (contains System.loadLibrary("onde"))
Always update consumer-rules.pro when adding new top-level UniFFI-exported
types or callback interfaces.
klibs.io Discovery
klibs.io is a JetBrains-maintained search platform for
Kotlin Multiplatform libraries. It auto-indexes from Maven Central.
Requirements for listing
| Requirement | How we satisfy it |
|---|
| Open source on GitHub | ondeinference/onde is public |
| At least one artifact on Maven Central | Published via Vanniktech plugin |
Artifact has kotlin-tooling-metadata.json | KMP plugin generates this automatically |
POM has valid GitHub url or scm.url | scm { url.set("https://github.com/ondeinference/onde") } |
Timeline
- New libraries appear within one month of first Maven Central publish
(public index update frequency).
- New versions of already-indexed libraries appear the next day.
- No manual submission needed — discovery is fully automatic.
Maven Central Publishing (CI)
Trigger: push a semver tag matching [0-9]+.[0-9]+.[0-9]+.
git tag 1.0.1 && git push origin 1.0.1
CI (release-sdk-kotlin.yml) steps:
- Install Rust stable with all 4 Android targets + host target
- Cache NDK 26.1.10909125
- Install
cargo-ndk
build-android.sh --release → .so files
build-jvm.sh --release → .dylib / .so
- Build
uniffi-bindgen
generate-bindings.sh → onde.kt
- Validate tag ==
Cargo.toml version
./gradlew :lib:publishAndReleaseToMavenCentral
- Upload artifacts as GitHub Release assets
Version Synchronisation Rule
VERSION_NAME in sdk/kotlin/gradle.properties MUST match version in the
root Cargo.toml. The CI workflow enforces this and fails fast on mismatch.
Never bump one without the other.
Common Pitfalls
| Pitfall | Fix |
|---|
dirs::home_dir() panics on Android | setup() must be called before any Rust model load. OndeInference.loadDefaultModel() calls it automatically. |
Generated onde.kt not found at compile time | Run scripts/generate-bindings.sh. The file is gitignored and must be regenerated after every Rust API change. |
.so files missing from AAR | Run scripts/build-android.sh. The androidMain/jniLibs/ directory is gitignored. |
JVM UnsatisfiedLinkError at runtime | Run scripts/build-jvm.sh to bundle the native lib in JAR resources, or add libonde.dylib to java.library.path. |
| UniFFI version mismatch | Keep uniffi = "=0.31.0" identical in Cargo.toml, build-deps, and uniffi-bindgen/Cargo.toml. |
| R8 strips UniFFI bridge at runtime | Ensure consumer-rules.pro is present in the AAR and includes -keep class uniffi.onde.**. |
System.loadLibrary("onde") fails | The .so files must match the ABI of the device. Check that all 4 ABIs are built and included in androidMain/jniLibs/. |
Os.setenv not available | minSdk must be 26+. android.system.Os is available from API 21 but setenv specifically needs 26. |
Gradle fails with VERSION_NAME missing | Ensure gradle.properties contains VERSION_NAME=x.y.z. |
cargo-ndk not found in CI | Add cargo install cargo-ndk --locked step before the build step. |
ANDROID_NDK_HOME not set | Set it explicitly: echo "ANDROID_NDK_HOME=$ANDROID_SDK_ROOT/ndk/26.1.10909125" >> $GITHUB_ENV |
build-android.sh: line XX: arm64: unbound variable on macOS | Do not use Bash associative arrays. The script uses portable indexed arrays for ABI → Rust target mapping. |
Unresolved reference 'sun' / Unresolved reference 'Structure' in onde.kt | UniFFI 0.31.0 depends on JNA (com.sun.jna.*). Add implementation("net.java.dev.jna:jna:5.14.0@aar") for Android or implementation("net.java.dev.jna:jna:5.14.0") for JVM. |
| HuggingFace download fails on first load | The app must declare <uses-permission android:name="android.permission.INTERNET" />. |
Plugin [id: 'com.android.application'] was not found in :example | Open sdk/kotlin/ (not sdk/kotlin/example/) in Android Studio. Plugin versions are inherited from the root build.gradle.kts. |
Using singleVariant publishing DSL multiple times | This error means the old AndroidSingleVariantLibrary config is still present. Use KotlinMultiplatform(...) instead — the KMP plugin handles variant publishing internally. |
Convenience functions (OndeSampling.default() etc.) crash before OndeInference() created | These functions call into the native library via UniFFI. Always create an OndeInference instance first — its init block ensures the native library is loaded. |
Testing
Unit tests (host JVM — no Android device needed)
Pure Kotlin logic tests that don't call into Rust:
cd sdk/kotlin
./gradlew :lib:test
./gradlew :lib:jvmTest
Instrumented tests (requires Android device or emulator)
Tests that call into Rust via the generated bindings:
./gradlew :lib:connectedAndroidTest
For CI, use an emulator (x86_64 ABI) with reactivecircus/android-emulator-runner.
JVM integration tests
Tests that load the native library and run inference:
./sdk/kotlin/scripts/build-jvm.sh
cd sdk/kotlin && ./gradlew :lib:jvmTest
Dependency Graph
OndeInference.kt (shared)
├── PlatformSupport (interface, shared)
│ ├── AndroidPlatform (androidMain)
│ │ └── android.system.Os.setenv
│ └── JvmPlatform (jvmMain)
│ ├── JNA → libc.setenv
│ └── NativeLoader → extracts libonde from JAR resources
├── uniffi.onde.OndeChatEngine (generated onde.kt)
├── uniffi.onde.StreamChunkListener (generated onde.kt)
├── uniffi.onde.*Config / *Message (generated onde.kt)
│ └── com.sun.jna.Native (JNA)
│ ├── libonde.so (Android, per ABI)
│ └── libonde.dylib (macOS, from JAR resources)
│ └── mistralrs (Metal on macOS, Candle CPU on Android)
│ └── hf-hub (model download)
└── Convenience.kt (shared)
└── OndeSampling, OndeModels, OndeMessage
Distribution Registry
| Registry | Artifact | Import | Discovery |
|---|
| Maven Central | com.ondeinference:onde-inference | implementation("com.ondeinference:onde-inference:1.0.0") | search.maven.org |
| klibs.io | onde-inference | Same Gradle coordinate | klibs.io (auto-indexed) |
| GitHub Releases | onde-inference-<version>.aar + .jar | Direct download | github.com/ondeinference/onde/releases |
End-game for Kotlin developers
One line in build.gradle.kts:
implementation("com.ondeinference:onde-inference:1.0.0")
Example App
A working Jetpack Compose chat app lives at sdk/kotlin/example/. It is the
canonical way to see the SDK in action and the first thing to open when
verifying a new build.
Opening in Android Studio
Always open sdk/kotlin/ — the Gradle root. Do not open
sdk/kotlin/example/ directly. Android Studio will see both :lib and
:example as modules and sync correctly.
What the example covers
- Loading the default model with
loadDefaultModel() and showing load time
- Streaming replies token-by-token via
onde.stream() and Flow<StreamChunk>
- Multi-turn conversation history maintained in the Rust engine
- Clearing history without reloading the model
- Material3 dynamic colour theme with dark mode support
- Proper IME / navigation-bar inset handling
Gradle submodule rules
- No
settings.gradle.kts inside example/ — Gradle would treat it as
standalone, breaking plugin resolution and project(":lib").
- No plugin versions in
example/build.gradle.kts — inherited from root.
include(":example") in root settings.gradle.kts — required for Gradle.
project(":lib") as the SDK dependency — builds against local source.
The root build.gradle.kts declares all plugins used by any submodule:
plugins {
id("com.android.library") version "8.5.2" apply false
id("com.android.application") version "8.5.2" apply false
id("org.jetbrains.kotlin.android") version "2.0.21" apply false
id("org.jetbrains.kotlin.multiplatform") version "2.0.21" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false
id("com.vanniktech.maven.publish") version "0.28.0" apply false
}
Streaming pattern
val placeholderId = System.nanoTime()
_uiState.update { it.copy(messages = it.messages + UiMessage(id = placeholderId, ...)) }
val buffer = StringBuilder()
onde.stream(text).collect { chunk ->
buffer.append(chunk.delta)
_uiState.update { state ->
state.copy(messages = state.messages.replaceLast(
UiMessage(id = placeholderId, content = buffer.toString(), isStreaming = !chunk.done)
))
}
}
Running the example
cd sdk/kotlin
./scripts/build-android.sh
./scripts/generate-bindings.sh
./gradlew :example:installDebug
Or open sdk/kotlin/ in Android Studio and hit Run on the :example configuration.
Cross-Platform SDK Comparison
Onde ships SDKs for four platforms. The Kotlin SDK is one piece of the
distribution strategy:
| SDK | Primary Platform | GPU Backend | Package Manager | Discovery |
|---|
| Swift | iOS, tvOS, macOS | Metal | Swift Package Manager | Swift Package Index |
| Dart | Android, iOS (Flutter) | Platform-dependent | pub.dev | pub.dev search |
| Kotlin | Android, JVM (macOS) | Metal (JVM) / CPU (Android) | Maven Central | klibs.io |
| React Native | Android, iOS | Platform-dependent | npm | npmjs.com |
The Swift SDK is the primary distribution — it runs natively on Apple silicon
with Metal acceleration. The Kotlin JVM target provides the same Metal-backed
inference for Kotlin developers on macOS, while the Android target extends
reach to the Android ecosystem.
Update this skill when adding KMP targets (e.g. Kotlin/Native for iOS),
changing the source set structure, or modifying the Maven Central publishing
pipeline.