Android RunAnywhereAI example

August 17, 2026 · View on GitHub

This file applies to RunanywhereAI/runanywhere-android, a standalone Android app that consumes the RunAnywhere Kotlin SDK from Maven Central. Run commands from the repository root unless noted otherwise.

Single Gradle module, :app. minSdk 24, compile and target SDK 37, Java 17, AGP 9.2.1, Gradle 9.6.0. The Gradle daemon needs JDK 21 (gradle/gradle-daemon-jvm.properties pins toolchainVersion=21) even though the app compiles against 17.

Common commands

./scripts/smoke.sh                         # Fast static SDK-usage check
./scripts/verify.sh                        # Strict debug APK build gate
./gradlew :app:assembleDebug               # Debug APK
./gradlew :app:testDebugUnitTest           # JVM tests
./gradlew :app:lintRelease                 # Release lint

Instrumented tests compile against the debug variant by default; pass -Prunanywhere.testBuildType=release to run them against the minified, signed release variant instead.

SDK dependency

The app consumes the RunAnywhere SDK entirely from Maven Central. There are no local AARs and no relative paths into any SDK source tree. Coordinates live in gradle/libs.versions.toml under the single runanywhere version:

CoordinateRole
io.github.sanchitmonga22:runanywhere-sdkCore SDK and the commons native library
io.github.sanchitmonga22:runanywhere-llamacppllama.cpp backend (LLM, VLM)
io.github.sanchitmonga22:runanywhere-onnxONNX Runtime (embeddings) and Sherpa-ONNX (STT, TTS, VAD) in one AAR
io.github.sanchitmonga22:runanywhere-qhexrt-androidQHexRT backend (Qualcomm Hexagon NPU), arm64 only

The four move in lockstep; never mix versions across them. To move to a new SDK release, bump runanywhere in gradle/libs.versions.toml, then regenerate both reproducibility files. The README's "SDK dependency" section has the exact procedure and its two traps: regenerate the checksums against a throwaway GRADLE_USER_HOME (a warm cache silently omits parent POMs and BOM metadata), and keep both the -linux.jar and -osx.jar com.android.tools.build:aapt2 entries so the one committed file satisfies CI and macOS developers alike.

Dependency verification and strict dependency locking are enforced in CI with no bypass flags. If a bump breaks them, regenerate the files; never re-add --dependency-verification=off or env -u CI to .github/workflows/ci.yml.

For a monorepo change that is not yet on Maven Central, publish it with publishToMavenLocal and build with -Prunanywhere.useLocalSdkAars=true --dependency-verification=lenient. Both flags are per-invocation only; never commit either, and never set them in CI.

The published POMs supply the SDK's own transitive dependencies (wire-runtime, okhttp, coroutines-core, okio, kotlin-stdlib, kotlinx-serialization-json, androidx core-ktx), so app/build.gradle.kts declares only what the app itself uses. The one exception is kotlinx-coroutines-android, declared directly so the Android artifact does not skew below the -core version the SDK POM pins.

Build configuration

app/build.gradle.kts reads backend configuration from environment variables first, then the gitignored local.properties. RUNANYWHERE_BASE_URL and RUNANYWHERE_API_KEY must be both set or both blank; the configuration phase fails otherwise. Blank means the SDK initializes in its development environment, which is the normal open-source path.

ABI filters differ per variant on purpose: release ships arm64-v8a only (QHexRT is arm64-only hardware and a single slice roughly halves the APK), debug adds x86_64 so emulator development still works with CPU backends.

bundleRelease and signReleaseBundle depend on verifyPlayRelease, which checks real upload signing, an HTTPS control-plane URL, and the upload certificate SHA-256 against UPLOAD_CERT_SHA256. bundleRelease also depends on generateReleaseSbom.

Production release requirements

A production build needs a real org-scoped API key and backend base URL — the same pair used by runanywhere-ios's RunAnywhereLocalSecrets.plist, runanywhere-electron's .env, and runanywhere-web's Vercel production env. Set them in the gitignored local.properties as runanywhere.apiKey / runanywhere.baseUrl; ask a maintainer for current production credentials. Never hardcode them in any committed file.

A production build must resolve the SDK only from Maven Central — never with -Prunanywhere.useLocalSdkAars=true, which is for local monorepo iteration only and must never be used for a release artifact. Before testing a "production" build, confirm no local Gradle property or env var is forcing the local-AAR path.

Emulator/CI passing is not sufficient. Smoke-test on real hardware: install the built APK on a connected physical device (adb devices, adb install -r ... or ./gradlew :app:installDebug) and confirm cold start, model-catalog population, and at least one real model load/inference — ideally one exercising the QHexRT/Hexagon NPU backend, since that is this app's differentiator and cannot be validated on x86 emulators.

A signed release AAB additionally needs the real Play upload keystore (KEYSTORE_PATH/KEYSTORE_PASSWORD/KEY_ALIAS/KEY_PASSWORD env vars, never committed or written to local.properties) and must pass verifyPlayRelease's upload-cert check.

Scripts

ScriptPurpose and normal use
smoke.shGrep-based check that the expected SDK entry points are still called from app/src/main. Set RUN_BUILD_GATES=1 to call verify.sh too.
verify.shDebug APK build gate with --dependency-verification strict. Needs only the Android SDK and network access.

smoke.sh matches source text, so it catches a deleted call site but proves nothing about whether the code compiles or runs. verify.sh is the real gate.

After editing these scripts, run bash -n scripts/*.sh, scripts/smoke.sh, and git diff --check.

app/src/main/java/.../ui/screens/solutions/SolutionsYaml.kt is generated from the canonical solution YAMLs in the SDK monorepo and committed verbatim. Regenerate it there, not here. Its header comment names paths from before the 0.20.17 monorepo restructure (examples/android/..., sdk/runanywhere-commons/...) and the script it names does not exist in this repo; the current sources live under core/ and bindings/ in RunanywhereAI/runanywhere-sdks.

Architecture notes

RunAnywhereApplication.onCreate initializes repositories, then runs SDK setup on an IO scope. Order matters: LlamaCPP.register() and ONNX.register() run before RunAnywhere.initialize(), or a concurrent loadModel() can fail with -422 while only the platform backend is registered. QHexRT.register() runs after initialize(), because it extracts DSP skels through the SDK-owned application Context. Both CPU backend registrations are wrapped in try/catch and report through BackendAvailability so a missing native library degrades the model picker instead of aborting setup.

Catalog seeding runs after GlobalState.markReady(), not before. The roughly 105 sequential models.register() JNI calls take about 13 s on a Snapdragon 8 Elite, which in front of the splash was the largest source of cold-start abandonment. A failure there is non-fatal: it means fewer rows in the picker, never the init-error screen.

Routes are type-safe @Serializable objects in ui/navigation/Destinations.kt. The drawer shows six ConsumerDestination entries in two groups; everything else is reached through MoreScreen. Model management is a sheet (ui/screens/models/ModelSelectionSheet.kt), not a route.

Design system

Brand primary is RunAnywhere orange #FF6900, the logo color. Theming is all Jetpack Compose Material 3: ui/theme/Color.kt (BrandOrange = 0xFFFF6900, the Primary* tonal ramp around that hue, and BrandGradient pairing it with BrandRed) and ui/theme/Theme.kt (lightColorScheme / darkColorScheme, no dynamic color, so the brand is guaranteed). res/values/colors.xml holds only structural values: black, white, and brand_window_background, which mirrors Neutral6 so the launch window paints the dark scheme's background and there is no light flash before Compose draws. When changing brand colors, edit Color.kt and keep it in sync with the RunAnywhere design guideline.