Version compatibility: Kotlin, AGP, Gradle & Compose decoupled
Since 0.4.0-alpha, Ketoy is decoupled from your project's toolchain on two independent axes:
- Kotlin / AGP / Gradle, fully decoupled by the embedded compiler (ADR-0004).
- 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 axis | Newer than Ketoy | Older than Ketoy | Floor |
|---|---|---|---|
| Kotlin | ✅ Irrelevant, embedded compiler runs pinned Kotlin in isolation | ✅ Irrelevant | Kotlin ≥ 2.0.0 |
| AGP | ✅ Irrelevant (one AGP-9 caveat below) | ✅ Irrelevant | Any AGP supporting the runtime AAR's compileSdk |
| Gradle | ✅ Irrelevant | ✅ Irrelevant | Gradle 8.6+ (Problems API) |
| Compose | ✅ Version-free for supported params; new features need an adapter | ✅ Version-free for supported params | Any 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.
┌─ 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 Ketoy | What happens | Render result |
|---|---|---|
Adds a param (e.g. Text.autoSize) | name not in catalog → dropped at encode; advisory emitted | Compose default applies; every known param lands at the right slot ✅ |
| Reorders params | each mapped by name → catalog index | correct ✅ |
| Removes a param Ketoy knows | param simply never encoded | adapter reads Default → Compose default ✅ |
| Renames a param | name match fails → dropped + advisory | Compose default + visible advisory |
Retypes a param (e.g. Color → ColorProducer) | conservative type guard drops it + advisory | Compose default + advisory ✅ |
| Matches Ketoy's Compose | name→index == position | byte-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@KetoyComposablebody. Use the app-specific ID range0x4000–0x7FFF. - 4.2, A new Compose component → custom composable adapter. Write a
KBCComposableAdapterat an app-specific id and register it onruntime.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.
// 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.
6. Recommended posture
- 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.