Ketoy

← SDK updates

Alpha

Version compatibility: Kotlin, AGP, Gradle & Compose decoupled

Jun 12, 2026 · SDK · 0.4.0-alpha

Since 0.4.0-alpha, Ketoy is decoupled from your project's toolchain on two independent axes:

  1. Kotlin / AGP / Gradle, fully decoupled by the embedded compiler (ADR-0004).
  2. Compose runtime version, decoupled for the supported parameter surface by name-based remapping (ADR-0005).

This update documents the impact of being on a newer (Kotlin 2.2 / 2.3, newer Compose BOM) or older version than Ketoy's pinned toolchain, in both directions, and how to pull in features Ketoy doesn't ship yet.

Ketoy pins its own toolchain internally, Kotlin 2.0.21 + a matching Compose Compiler, and a Material3 adapter catalog generated against Compose BOM 2024.11.00 (Material3 1.3.x). The question this page answers is: what happens to your app when your versions differ from those?

The short version

Your axisNewer than KetoyOlder than KetoyFloor
Kotlin✅ Irrelevant, embedded compiler runs pinned Kotlin in isolation✅ IrrelevantKotlin ≥ 2.0.0
AGP✅ Irrelevant (one AGP-9 caveat below)✅ IrrelevantAny AGP supporting the runtime AAR's compileSdk
Gradle✅ Irrelevant✅ IrrelevantGradle 8.6+ (Problems API)
ComposeVersion-free for supported params; new features need an adapter✅ Version-free for supported paramsAny Compose whose param names/types match the catalog

The distinction that runs through this whole page: Ketoy frees the version of Compose, not the feature set. A newer Compose version is fine; a brand new Compose parameter or component Ketoy has no adapter for is a separate thing you opt into.

Quick answers

Does being on a newer (2.2/2.3) or older version affect your project?

Kotlin / AGP / Gradle, no impact, either direction. The embedded compiler runs pinned Kotlin 2.0.21 in an isolated classloader; your toolchain never sees it and vice-versa. Verified at Kotlin 2.2.10 / AGP 9.1.1 / Gradle 9.3.1. Floor is Kotlin ≥ 2.0.0. One ecosystem caveat documented: under AGP 9 you must drop the standalone kotlin.android plugin.

Compose, the version is free, the feature set is not. ADR-0005's name-based remapping makes any Compose version render correctly for the parameters in Ketoy's catalog, forward and backward. Params your Compose adds / renames / retypes are dropped to the Compose default with a build advisory, never a silent wrong render.

You're on 2.2/2.3 and want features Ketoy doesn't ship, how do you add them?

Three paths, none needing a Ketoy release:

  • New behaviour / Android API → register a capability (a 0x4000+ stub).
  • New Compose component, or a new param on an existing component you need now → write a custom composable adapter compiled against your Compose and register it on runtime.adapterRegistry.
  • New value type → custom constructor adapter.

The only thing requiring a Ketoy release is adding a param to Ketoy's own generated Material3 adapters, and the custom-adapter path sidesteps even that.

Are both directions, newer and older, really handled the same way?

Yes. Forward (newer Compose, extra/reordered params) and backward (older Compose, fewer params) are handled by the same name-match. Each argument is mapped by its param name to the catalog's sourceIndex; a name the catalog doesn't know is dropped to the Compose default with an advisory. The full behaviour-per-scenario table is reproduced in §2.

If something is removed in 2.2, does it still work?

Two cases, distinguished honestly:

  • Source-level / default-hidden param removal → yes. The adapter reads Default, so the Compose default applies and it renders correctly.
  • A genuine binary-incompatible removal of a whole composable / required param → out of scope. That's a Compose ABI break beyond the matrix (one Compose itself avoids within its stability guarantees), and even then the native fallback is the steady-state safety net, so it degrades to the native render, not a crash.

1. The Kotlin / AGP / Gradle axis, fully decoupled

Impact of any version, newer or older: none.

Before 0.4.0, the KBC compiler plugin attached to your compileReleaseKotlin task, forcing your app onto Ketoy's exact Kotlin version. ADR-0004 removed that: a Gradle worker now drives a pinned K2JVMCompiler in a classloader fully isolated from your build. Your Kotlin compiler never loads any Ketoy compiler code; Ketoy's pinned Kotlin never sees your toolchain.

build pipeline
┌─ your build (Kotlin 2.2 / 2.3, AGP 9, Gradle 9) ─────────────┐
│   compileReleaseKotlin  →  your DEX (native fallbacks)        │
│                                                               │
│   ketoyBundle task ──► isolated worker classloader            │
│                          pinned K2JVMCompiler (2.0.21)         │
+ KBC IR plugin + Compose plugin      │
│                          → signed .ktx                         │
└───────────────────────────────────────────────────────────────┘

Verified in production: the reference app built at Kotlin 2.2.10 / AGP 9.1.1 / Gradle 9.3.1, AGP a full major ahead of Ketoy's 8.13, compiled its .ktx and rendered the KBC screen on device.

  • Forward support (you on Kotlin 2.2 / 2.3): works unchanged. The embedded compiler tolerates newer dependency metadata on your compile classpath via the standard escape hatches (skipMetadataVersionCheck / skipPrereleaseCheck / allowUnstableDependencies, the same flags KSP and kapt use). The embedded compile only reads API signatures to lower your @Ketoy* closure; it never links bytecode internals, so newer metadata is safe.
  • Backward support (you on Kotlin 2.0.x / 2.1.x): works unchanged, your metadata is older than the pinned frontend, which is always readable. Floor: Kotlin ≥ 2.0.0, because the runtime AAR's stdlib is 2.0-based.

The one AGP-9 caveat (ecosystem, not Ketoy)

AGP 9 ships “built-in Kotlin” and removed the legacy BaseExtension the standalone org.jetbrains.kotlin.android plugin casts to. Under AGP 9 you must not apply org.jetbrains.kotlin.android, AGP drives Kotlin itself. Keep com.android.application + org.jetbrains.kotlin.plugin.compose. This is a general AGP-9 migration step independent of Ketoy; the dev.ketoy.compiler plugin and the embedded compile are unaffected.

The embedded Kotlin only matters to you at Ketoy's own bump cadence (yearly major bumps in Q1, plus out-of-band bumps for security/ABI breaks). It governs what Kotlin syntax you can write inside a @KetoyComposable body (the KBC subset), not what version your app uses. That subset is basic language surface (control flow, arithmetic, when, data classes, remember, coroutines), so the band almost never moves in a way you'd notice.

2. The Compose axis, version-free for the supported surface

Ketoy lowers each @Composable call into a COMPOSABLE_CALL opcode carrying a sparse list of (paramIndex, KBCValue) pairs. Before 0.4.0 that paramIndex was positional in your Compose signature, but the prebuilt runtime adapter read it as positional in Ketoy's catalog signature. They were equal only when your Compose version matched Ketoy's.

The classic failure: Material3 1.4.0 inserted autoSize into Text, shifting fontFamily from position 11 → 12. The emitter wrote fontFamily at 12; the adapter read slot 11 → wrong slot → the custom font silently fell back to the default face, while a drawable on a stable Image signature rendered fine.

ADR-0005 made the parameter name the version-invariant identity. The emitter now maps each argument's param name → the catalog's sourceIndex and encodes in catalog space. The runtime reads catalog space. Your Compose version only affects emit-time resolution, never the wire contract.

Your Compose, relative to KetoyWhat happensRender result
Adds a param (e.g. Text.autoSize)name not in catalog → dropped at encode; advisory emittedCompose default applies; every known param lands at the right slot ✅
Reorders paramseach mapped by name → catalog indexcorrect ✅
Removes a param Ketoy knowsparam simply never encodedadapter reads Default → Compose default ✅
Renames a paramname match fails → dropped + advisoryCompose default + visible advisory
Retypes a param (e.g. ColorColorProducer)conservative type guard drops it + advisoryCompose default + advisory ✅
Matches Ketoy's Composename→index == positionbyte-identical to the old behaviour ✅

So both forward (newer Compose with extra/reordered params) and backward (older Compose with fewer params) are handled by the same name-match. Dropped params degrade to the Compose default and emit a build-time advisory, never a silent wrong render.

Verified on device

rebuilt at Compose BOM 2026.02.01 / Material3 1.4.0 (newer than the catalog) emitted the expected advisories (Text.autoSize and a TopAppBarDefaults color dropped as unknown-to-catalog) and the Courgette font rendered correctly, fontFamily mapped by name despite autoSize shifting its position.

What ADR-0005 frees is the Compose version for the supported parameter surface. What it does not do, by design:

  • A brand-new Compose parameter Ketoy has no adapter coverage for is dropped (Compose default applies).
  • A brand-new Compose component Ketoy has no adapter for can't be called from a KBC body at all, the validator rejects it as an unregistered composable.

Both are covered by §4.

3. "Something was removed in my Compose version", does it still work?

Source-level / default-hidden removal (the common case): yes. When Compose deprecates-then-hides a parameter, your KBC source either never set it (it falls to the Compose default) or the encoder simply doesn't encode it. The adapter reads Default. Renders correctly.

Binary-incompatible removal (rare): out of scope, it's a Compose ABI break, not a Ketoy one. The prebuilt adapters link against the single Compose present at runtime (yours). If your Compose genuinely binary-removes a whole composable or a required parameter, the adapter fails to link (NoSuchMethodError), but Compose maintains binary compatibility for @Composable functions across versions, so this effectively does not arise for the Material3/Foundation surface Ketoy ships.

The safety net always applies

Whatever happens to the KBC render, the native fallback is the steady state. KetoyScreen(entryPoint = "X") renders the native X() function, compiled by your Kotlin against your Compose, whenever the bundle is absent, corrupt, incompatible, or fails to load. Even a worst-case Compose ABI break degrades to the native render, not a crash.

4. Adding features Ketoy doesn't ship yet

You want a component or parameter Ketoy's catalog doesn't cover. You have three paths, none require waiting for a Ketoy release.

  • 4.1, A new Android API / side effect → capability. If it's a behaviour (haptics, a platform API, analytics, a network call, a DataStore key), register it on your KetoyCapabilityProvider, expose it via a @KetoyCapabilityStub, and call it from your @KetoyComposable body. Use the app-specific ID range 0x4000–0x7FFF.
  • 4.2, A new Compose component → custom composable adapter. Write a KBCComposableAdapter at an app-specific id and register it on runtime.adapterRegistry. Because you compile it against your Compose, it picks up whatever version/feature you're on. This is also the escape hatch for a new parameter on an existing component you need now.
  • 4.3, A new Compose value type (constructor) → custom constructor adapter. For a new constructable value (TextStyle-like, a shape, a config object), write a custom constructor adapter the same way.
MyChartAdapter.kt
// Pick an app-specific adapter id (0x4000+), compiled against YOUR Compose.
val MyChartAdapter = KBCComposableAdapter(
  id = 0x4010.toShort(),
  fqName = "com.example.ui.MyChart",
  paramCount = 3,
) { p ->
  MyChart(
      data      = p.getString(0),
      modifier  = p.getModifier(1),
      lineColor = p.getColor(2),
  )
}

// Register at startup, alongside the generated Material3 adapters.
runtime.adapterRegistry.register(MyChartAdapter)

The one thing you can't do from consumer config alone: add a new param to one of Ketoy's own generated Material3 adapters, that means regenerating the catalog, which is a Ketoy release. The custom-adapter path above sidesteps it: register a parallel adapter at an app-specific id and get the param immediately.

5. Reading the build-time advisories

When your Compose version diverges from the catalog, the embedded compiler tells you exactly what it dropped, via the Gradle Problems API + task log:

  • Unknown param (WARNING), "<Composable>.<param> is not in the Ketoy adapter catalog (consumer Compose newer than supported); using the Compose default."
  • Type-mismatch drop (WARNING), "<Composable>.<param> type changed (<consumerType> vs catalog <catalogType>); skipped to avoid a wrong-typed render."
  • Per-bundle summary (INFO), e.g. "346 argument(s) encoded; 2 unknown, 0 type-mismatched.", "fully covered" means your version matches the catalog surface exactly.

These make the coverage boundary observable at build time, a divergent Compose version surfaces as a visible advisory instead of a silent wrong render.

  • Bump Kotlin / AGP / Gradle freely. No Ketoy constraint (mind the AGP-9 plugin caveat in §1).
  • Bump Compose freely for the supported surface. Newer/older versions render correctly for every param in the catalog; watch the build advisories for anything dropped.
  • If you depend on a brand-new Compose param or component, ship a custom adapter (§4.2), immediate, no Ketoy release needed.
  • If you depend on a new behaviour/API, ship a capability (§4.1).
  • Keep your native fallbacks honest. They are the steady state and your insurance against any Compose ABI surprise.
Kotlin-decoupledAGP / Gradle-freeCompose-version-freename-based remapnative fallback