Today we are introducing KetoyVM, a Kotlin program execution runtime for Android. You write the same Jetpack Compose you write today, full composables, full ViewModels, Hilt and Room functions exposed by the host app (updates still require a Play Store release), NavController navigation, coroutines and Flow, and you ship it as a binary bundle from your server. Your host app downloads a .ktx file and the KetoyVM runtime executes it natively inside your app. Real Compose. Real structured concurrency. Real Room queries. Real navigation. Nothing is translated. Nothing is simulated.
The app is the operating system. The server ships programs. Kotlin is the language. KBC is the binary.
This is our first post, so we want to use it to explain exactly what we built, how it works, and what it changes for Android teams.
§ 01What KetoyVM is
KetoyVM is three things working together: a Kotlin compiler plugin, a compact bytecode format called KBC, and an on-device runtime that executes it. You install the Gradle plugin in your feature module. You write Kotlin the way you always do. You run ./gradlew ketoyBundle. You get a .ktx file. You upload it to your CDN. Every device running your host app fetches the new version the next time the user opens that screen.
The important thing to understand is that this is not a UI templating system. The whole Kotlin/Android stack you rely on is inside the bundle. A screen is not a tree of components, it is a program. It has state. It has a ViewModel. It injects a repository through host-exposed Hilt functions. It calls a Retrofit API and observes a Room Flow exposed by the host app. Hilt and Room themselves are not updated over the air because updating them requires a Play Store release. It navigates to another screen. It uses LaunchedEffect, remember, derivedStateOf, rememberSaveable. All of that ships in the bundle. All of that runs on the device.
Here is a working sign-in screen, exactly as you would write it in a native Compose project:
@KetoyEntryPoint @Composable fun SignInScreen(nav: NavController) { val vm = ketoyViewModel<SignInViewModel>() val state by vm.state.collectAsState() Column( modifier = Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { Text("Sign In", style = MaterialTheme.typography.headlineLarge) Spacer(Modifier.height(32.dp)) OutlinedTextField( value = state.email, onValueChange = { vm.onEmailChanged(it) }, label = { Text("Email") }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Email, imeAction = ImeAction.Next, ), shape = RoundedCornerShape(12.dp), ) Button( onClick = { vm.signIn(onSuccess = { nav.navigate("home") }) }, enabled = !state.isLoading, modifier = Modifier.fillMaxWidth().height(52.dp), ) { if (state.isLoading) CircularProgressIndicator(Modifier.size(24.dp)) else Text("Sign In") } } } @HiltViewModel class SignInViewModel @Inject constructor( private val authRepo: AuthRepository, private val userDao: UserDao, ) : ViewModel() { val state = userDao.observeCurrent() .map { UiState(email = it?.email ?: "") } .stateIn(viewModelScope, SharingStarted.Lazily, UiState()) fun signIn(onSuccess: () -> Unit) = viewModelScope.launch { authRepo.signIn(state.value.email, state.value.password) .onSuccess { onSuccess() } } }
There is nothing KetoyVM-specific in that code except the @KetoyEntryPoint annotation that marks which composable is the bundle's entry. The @HiltViewModel still uses the host app's Hilt graph. userDao.observeCurrent() returns a real Flow<User?> from the host app's Room. KetoyVM does not update Hilt or Room; it exposes their functions for new features because updating them requires a Play Store release. viewModelScope.launch is real structured concurrency. nav.navigate("home") is a real NavController call. The developer does not learn a new framework. The developer writes Android.
§ 02Everything a real feature needs, shipped in the bundle
The point of KetoyVM is not "UI on the server." It is "features on the server." A feature in a modern Android app is a vertical slice: presentation, state, dependency graph, data, side effects, navigation. If any one of those pieces cannot be delivered over the air, the whole thing has to go through a Play Store release, and the exercise is meaningless. KetoyVM delivers the whole slice. Here is what comes through the bundle:
Text, all 22 of TextField. Real slot table, real recomposition, real Skia.KetoyVirtualViewModel hosts your logic. SavedStateHandle persistence. onCleared cancels coroutines. Config-change survival is built in.KetoyCapabilityProvider. Your bundle's @HiltViewModel classes get repositories, services, and DAOs injected exactly as they would natively. Hilt stays in the host app; KetoyVM does not update Hilt because updates require a Play Store release.Flow<List<User>> crosses into KBC as a real Flow and connects to Compose via collectAsState. Room stays in the host app; KetoyVM does not update Room because updates require a Play Store release.SUSPEND_POINT, FLOW_COLLECT, WITH_CONTEXT. Structured concurrency with real parent/child cancellation. Dispatchers.IO and friends.This is why we keep saying programs instead of layouts. When you ship a KetoyVM bundle, you are not patching the UI and leaving the logic stuck at the last Play Store version. You are replacing the feature end-to-end. A new onboarding flow. A new ViewModel with a new signup path. A new host-exposed dependency. A change to the host-exposed Room query that drives the home screen. Hilt and Room stay in the host app; KetoyVM exposes their functions for new features because updating them requires a Play Store release. All of it in one file.
§ 03What the build pipeline actually does
When you run ./gradlew ketoyBundle, here is what happens. Our Kotlin compiler plugin hooks into the K2 IR phase and lowers your Compose IR into KBC, a register-based bytecode built for this specific purpose. Register-based because that is what DEX is, and because register-based VMs issue roughly 30% fewer instructions than stack-based ones, the same decision ART made, for the same reason.
KBC has opcodes that speak Kotlin natively: SUSPEND_POINT, RESUME_VALUE, FLOW_COLLECT, COMPOSE_REMEMBER, LAUNCHED_EFFECT, COLLECT_AS_STATE. And two more that do the heavy lifting for Compose interop: COMPOSABLE_CALL, which invokes a composable via a KSP-generated adapter, and CONSTRUCT_JVM, which builds real Compose objects like TextStyle, KeyboardOptions, and Shape into runtime registers. The adapters are auto-generated against the Compose and Material3 classpath, which is how we cover every parameter of every component, including the ones that shipped in last week's Material3 release, without maintaining a component registry by hand.
The bundle is Brotli-compressed, signed with Ed25519, and carries a manifest of every adapter, constructor, and capability it references. At load time the runtime verifies the signature, checks that every referenced adapter and capability exists on this device, and only then begins execution. If anything is missing, you get a single KetoyMissingAdapterException with the list of missing IDs, fail-fast, before a single line of KBC runs.
On-device, for hot functions, a tiered JIT generates DEX locally from KBC. This is the same thing ART does constantly, and it is explicitly legal under Play Store policy because the DEX is generated on the device, not downloaded from a server. We never ship executable code. We ship Kotlin programs.
§ 04The numbers we engineered this to hit
These are the performance targets KetoyVM's architecture is designed to meet. They are architectural commitments, wired into the repo as CI benchmarks, not press-release numbers. Every PR that regresses one has to justify it:
.ktx, Ed25519 verified, adapter manifest checked.§ 05What this does to a sprint
Everything above is infrastructure. Here is what the infrastructure is in service of. If you are a tech lead or engineering manager, this is the section that matters, because it changes the arithmetic of how your team plans work.
Think about your last twelve months. How many updates did your Android team ship? For a moderately active consumer or SaaS app, twenty is a reasonable estimate, roughly one meaningful release every two to three weeks, counting minor and mid-size features. Now look at where the time actually went:
And this math only counts the formal wait. It does not count the quieter tax: the features that never get shipped because they are not big enough to justify taking up a release slot, the copy fixes a PM asked for three weeks ago that are still waiting for the next train, the onboarding tweak a designer wants to try that is not worth starting a staged rollout for. Every batching decision is a small deferral, and those deferrals compound.
The quieter consequence is what this does to your sprint cadence. When every merged feature sits in a review queue, teams batch. They combine a copy change, a small bug fix, a new onboarding step, and a checkout tweak into one release so the fixed review overhead amortizes across multiple units of work. Batching feels efficient but it is actually expensive: it delays small wins behind big ones, and it couples unrelated risks into one rollback decision. When you remove the queue, the batching goes away. Sprints stop being "what can we land in the next release train" and start being "what can we build, test, and ship this week." The grain of planning shrinks from sprints to afternoons.
The second-order effect is even better. Because shipping is cheap, you ship more. An experiment that would not have justified the fixed cost of a release now justifies a bundle upload. A copy change a PM has been asking about for three weeks goes out in ten minutes. A host-exposed Room query that is slow for power users gets patched on Tuesday instead of in the next release. Twenty updates a year becomes forty, then sixty, without adding engineers, because the engineers you already have stop waiting.
§ 06What Android teams can do on day one
These are the concrete shifts teams see when they put their first screen on KetoyVM. None of them are theoretical, they are direct consequences of what the runtime does.
Ship features, not just UI, without a Play Store release
A KetoyVM bundle is a feature. If you change your ViewModel, the Hilt- or Room-backed calls exposed by the host app, your navigation flow, or your Compose tree, all of it goes out in the same .ktx. Hilt and Room themselves stay in the host app; KetoyVM exposes their functions for new features because updating them requires a Play Store release. The only thing you cannot change over the air is the host app's set of registered capabilities, the Android APIs the bundle is allowed to call, and in practice those stabilize early in a project and rarely change. Everything else ships from CDN.
Faster sprints, because "done" actually means "shipped"
The end of a sprint should be the end of the work, not the start of a ten-day wait for Google. When bundle upload is the last step, sprint demos become live rollouts. A bug caught in code review gets fixed and redeployed in the same afternoon. A design tweak that lands on a Wednesday reaches users on a Wednesday. Teams we have talked to describe this as the first time mobile has felt like web.
Rollback that is the same operation as roll-forward
A P0 in a native release is a four-day recovery. In KetoyVM, you re-point the bundle URL to the previous version. On the next fetch, users are on the good code. The bundle-verification guarantees (Ed25519 signature, adapter manifest, capability manifest, fail-fast on anything missing) are the same going backwards as forwards, which means rolling back is as safe as rolling forward was. This alone rewrites your incident response playbook.
A/B test at the grain of a single screen
A variant is a second .ktx. Route 10% of users to checkout@v2.ktx at the CDN edge, leave the rest on the current version, watch the funnel. No feature flag sprawl. No two code paths to maintain in the host app. When the winner is clear, promote the bundle and retire the other. The fixed cost of "running an experiment" collapses to "uploading a file," and teams that currently run three or four experiments a quarter can realistically run an order of magnitude more.
Personalize per user, not per segment
Serve different bundles to different users. Premium tier gets a richer checkout. First-time users get a guided flow. A specific cohort in a specific region gets a compliance-adjusted variant. The host app does not branch on any of this; it asks for the bundle for this user, on this screen, right now. Personalization moves out of client code, where it accumulates as tech debt and rarely gets removed, and into server-side policy you can observe, change, and retire cleanly.
User engagement that keeps up with your product thinking
Engagement on mobile dies in the gap between we noticed something in the data and users see a change. When that gap is weeks, the insight goes stale, the cohort has churned, the season has moved, the PM has three new theories. When that gap is an afternoon, engagement becomes a tight loop. You see a drop-off on step three of onboarding on Monday morning and you ship a fix that afternoon, watch the funnel on Tuesday, iterate Wednesday. That is the loop every web team has and no mobile team has. KetoyVM is the shortest honest path to closing it.
Keep your Android team an Android team
We held this one for last because it is the one most leads end up caring about most. Every other "dynamic delivery" path we have watched teams take ends with the engineering org splitting into two tiers: the people who write the shell app in Kotlin, and the people who write the "dynamic content" in JSON, Lua, JavaScript, or a homegrown DSL. Two stacks, two hiring pipelines, two sets of idioms, a quiet caste system. KetoyVM is the same language end-to-end. Your Android engineers stay Android engineers. Your CI stays Gradle. Your code review stays a Kotlin review. The operational burden of a dynamic-delivery system collapses into another Gradle task.
Every feature your team wanted to ship this quarter but could not justify the release overhead of is now a file you upload. Every bug you watched sit in staged rollout is now a re-deploy. Every experiment you did not run because it was not worth the release cost is now cheap. Over a year, that compounds into a different product.
§ 07Where we are, and what is next
KetoyVM is being built in public. The compiler plugin, the VM, the KSP adapter generator, the bundle format, and the tooling are landing on GitHub as they stabilize. Every performance target above is in the repo as a CI benchmark. The architecture decision log, why register-based, why a custom bytecode instead of DEX, why KSP-generated adapters, why Ed25519, is committed next to the code. We are not running a closed beta. We are running a long, loud build.
If you lead an Android team that ships more than the release train wants to let you, if you have a PM who has been waiting two weeks on a three-line copy change, if you want to experiment more than your release cadence allows, we want to work with you. The earliest integrations will shape what the SDK looks like.
Build on KetoyVM
Read the docs, clone the repo, or talk to us about integrating. The early-partner program is open.
Aditya
Writes about the big picture of what KetoyVM does to teams and products.