Installation
This page walks you through adding Ketoy 0.4.16-alpha to an Android project — either via the **ketoy CLI** (fastest path, recommended for new projects) or by editing Gradle files yourself.
Requirements
As of 0.4.16-alpha, Ketoy no longer pins your project to a single Kotlin /
AGP / Gradle / Compose combination. The compiler plugin ships its own embedded
Kotlin compiler (ADR-0004) that runs in an isolated worker, and the Compose
version is decoupled for the supported parameter surface via name-based
remapping (ADR-0005). You bump these axes freely, only the floors matter:
| Tool | Minimum | Notes |
|---|---|---|
| Kotlin | 2.0.0 | Any newer version (2.1, 2.2, 2.3) works, your toolchain never loads Ketoy's pinned compiler. |
| Android Gradle Plugin | any supporting your compileSdk | Verified up to AGP 9.1.1. See the AGP-9 plugin caveat below. |
| Gradle | 8.6 | Needs the Problems API for build-time advisories. |
| Jetpack Compose BOM | any | Catalog generated against BOM 2024.11.00 (Material3 1.3.x); newer/older render correctly for every catalogued param. |
| JDK | 17 | |
minSdk | 26 | |
compileSdk | 35 or higher |
Ketoy pins Kotlin 2.0.21 internally for KBC emission, that governs only
what Kotlin syntax you can write inside a @KetoyComposable body (the KBC
subset), not what version your app uses. For the full picture of what being on
a newer or older version means in both directions, see the
Version compatibility update.
AGP-9 plugin caveat
AGP 9 ships built-in Kotlin and removed the legacy BaseExtension that the
standalone org.jetbrains.kotlin.android plugin casts to. Under AGP 9 you must
not apply org.jetbrains.kotlin.android, keep com.android.application
org.jetbrains.kotlin.plugin.compose. This is a general AGP-9 migration step independent of Ketoy; thedev.ketoy.compilerplugin is unaffected.
Path 1, ketoy CLI (recommended)
The CLI scaffolds everything for you with diff-and-confirm on every file
edit. It's an AI-powered agent, but the init command runs deterministic
template edits, no LLM in the loop.
Install
npm install -g ketoy-dev # binary is `ketoy`; npm package is `ketoy-dev`
ketoy versionRequires Node.js ≥ 20.
Authenticate (only needed for chat / migrate / doctor)
ketoy auth anthropic # paste your API key when prompted
# or: openai | google | mistral | groq | xai | openrouter | ollamaCredentials are stored at ~/.ketoy-cli/config.json with mode 0600.
The CLI refuses to print them back.
Initialize a project
From inside an Android project root (the folder with settings.gradle.kts
and app/build.gradle.kts):
ketoy initThe CLI plans every edit, shows a unified diff, asks for confirmation, then:
- Inserts
id("dev.ketoy.compiler") version "0.4.16-alpha"into your app module's plugins block. - Appends the Ketoy dependencies (BOM + runtime + capabilities + adapters).
- Appends a
ketoy { exportFromAppModule = true; bundleId = "main"; bundleVariant = "release"; … }extension block. - Creates
MyApplication.kt(or merges into your existingApplicationclass) with the runtime bootstrap. - Wraps
MainActivity'ssetContent { … }body with aKetoyScreen { /* native fallback */ }(you can opt out with--no-install-screen). - Creates
HelloKetoyScreen.kt, a starter@KetoyEntryPoint @KetoyComposable. - Creates
app/ketoy-capabilities.json(empty, fill in as you add custom capabilities). - Appends
**/keys/*-private.keyto.gitignore.
Then:
./gradlew :app:assembleDebugYou've got a real .ktx bundle running inside your APK.
CLI cheat sheet
ketoy init [--install-screen | --no-install-screen] [--hilt | --no-hilt]
ketoy chat # interactive AI agent
ketoy migrate <file> # AI-driven Compose → KBC port
ketoy doctor [task] # diagnose Gradle errors
ketoy build [--variant bundle|debug|release]
ketoy analyze <path.ktx> [--manifest] [--strings] [--json]
ketoy config list | get | set
ketoy auth [provider] [--remove] [--list]
ketoy versionPath 2, Manual setup
If you'd rather wire Ketoy by hand, here's the full setup.
1. Top-level settings.gradle.kts
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}2. App module build.gradle.kts
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("dev.ketoy.compiler") version "0.4.16-alpha" // ← Ketoy plugin
id("kotlin-kapt") // if using Hilt
id("dagger.hilt.android.plugin") // if using Hilt
}
android {
namespace = "com.example.myapp"
compileSdk = 35
defaultConfig {
applicationId = "com.example.myapp"
minSdk = 26
targetSdk = 35
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions { jvmTarget = "17" }
buildFeatures { compose = true }
}
dependencies {
val ketoyBom = platform("dev.ketoy.vm:ketoy-bom:0.4.16-alpha")
implementation(ketoyBom)
implementation("dev.ketoy.vm:ketoy-runtime")
implementation("dev.ketoy.vm:ketoy-annotations")
implementation("dev.ketoy.vm:ketoy-capabilities-core")
implementation("dev.ketoy.vm:ketoy-capabilities-navigation")
implementation("dev.ketoy.vm:ketoy-adapters-material3")
// Optional, Hilt integration
implementation("dev.ketoy.vm:ketoy-hilt")
// Standard Compose deps
implementation(platform("androidx.compose:compose-bom:2024.10.00"))
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui")
implementation("androidx.activity:activity-compose:1.9.3")
// Test
testImplementation("dev.ketoy.vm:ketoy-test")
}
// Ketoy compiler-plugin configuration
ketoy {
exportFromAppModule.set(true) // emit .ktx into THIS app module
bundleId.set("main") // produces main.ktx
bundleVariant.set("release") // attach plugin to release variant
capabilityRegistryFile.set(
file("ketoy-capabilities.json")
)
signingKeyFile.set(
file("keys/main-private.key") // 32-byte Ed25519 seed
)
minAppVersion.set(0) // gates bundle activation per app version
}3. Generate an Ed25519 key pair
mkdir -p app/keys app/src/main/assets/ketoy/keys
# Generate private key (32-byte raw seed)
openssl genpkey -algorithm Ed25519 -outform DER -out /tmp/ed25519.der
tail -c 32 /tmp/ed25519.der > app/keys/main-private.key
# Derive public key (32 bytes, the trust anchor shipped in the APK)
openssl pkey -in /tmp/ed25519.der -inform DER -pubout -outform DER \
| tail -c 32 > app/src/main/assets/ketoy/keys/main-public.keyThen add to .gitignore:
**/keys/*-private.key4. app/ketoy-capabilities.json
Start empty, populate as you wire custom capabilities (see Custom Capability):
{
"version": 1,
"allowedStdlibFqNames": [],
"capabilities": []
}5. Bootstrap the runtime
Create MyApplication.kt:
package com.example.myapp
import android.app.Application
import dev.ketoy.adapters.material3.registerGeneratedAdapters
import dev.ketoy.adapters.material3.registerGeneratedConstructors
import dev.ketoy.runtime.KetoyConfig
import dev.ketoy.runtime.KetoyRuntime
import dev.ketoy.runtime.bundle.KetoyBundleLoader
import dev.ketoy.runtime.capability.CapabilityRegistry
import dev.ketoy.runtime.security.KetoyKeystore
class MyApplication : Application() {
lateinit var runtime: KetoyRuntime
private set
lateinit var bundleLoader: KetoyBundleLoader
private set
override fun onCreate() {
super.onCreate()
val publicKey = KetoyKeystore.loadFromAsset(
this, "ketoy/keys/main-public.key"
)
val capabilities = CapabilityRegistry()
// capabilities.registerCoreCapabilities(this), see Networking / DataStore guides
runtime = KetoyRuntime(
capabilityRegistry = capabilities,
config = KetoyConfig(
enableSignatureVerification = true,
publicKey = publicKey,
enableJIT = true,
dexCacheDir = codeCacheDir,
)
)
runtime.adapterRegistry.registerGeneratedAdapters(runtime)
runtime.constructorRegistry.registerGeneratedConstructors()
bundleLoader = KetoyBundleLoader(runtime, this)
}
}Register it in AndroidManifest.xml:
<application android:name=".MyApplication" ...>6. Wire into MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val app = application as MyApplication
setContent {
MyAppTheme {
CompositionLocalProvider(
LocalKetoyCapabilityRegistry provides app.runtime.capabilityRegistry,
LocalKetoyBundleLoader provides app.bundleLoader,
) {
KetoyScreen(
bundleSource = KetoyBundleSource.Asset("ketoy/main.ktx"),
entryPoint = "HelloKetoyScreen",
nativeFallback = { HelloNativeFallback() }
)
}
}
}
}
}
@Composable
private fun HelloNativeFallback() {
Surface { Text("Ketoy bundle not available, using native fallback.") }
}Verify the install
./gradlew :app:ketoyBundle # produces app/src/main/assets/ketoy/main.ktx
./gradlew :app:assembleDebug # builds the APK
./gradlew :app:installDebug # installs onto a connected deviceYou should see the HelloKetoyScreen content on launch. If you instead
see the native fallback, the bundle didn't load, check adb logcat for
Ketoy / KetoyBundleLoader log lines.