Ketoy

← SDK updates

New

Ketoy 0.4.20-alpha: back on Maven Central

Jul 20, 2026 · SDK · 0.4.20-alpha

This is the first Maven Central release since 0.4.13-alpha. It ships the headline app-scoped shared runtime (shared ViewModels across screens) plus everything that landed in the 0.4.16-0.4.19 mavenLocal iterations, Material3 AlertDialog, FlowRow, Compose Pagers, and a batch of fixes.

Coordinates

build.gradle.kts
dependencies {
  implementation(platform("dev.ketoy.vm:ketoy-bom:0.4.20-alpha"))
  implementation("dev.ketoy.vm:ketoy-runtime")
  implementation("dev.ketoy.vm:ketoy-hilt")             // optional
  implementation("dev.ketoy.vm:ketoy-koin")             // optional (new)
  implementation("dev.ketoy.vm:ketoy-adapters-material3")
}

Drop-in upgrade from 0.4.x, everything below is additive and backward compatible. No wire-format, opcode, or @KetoyViewModel authoring change.

At a glance

Shared runtimePer-screen opt-outMaterial3 AlertDialogReal FlowRowCompose Pagersketoy-koin
AreaWhat's new
Shared runtimeKetoyRuntimeHost { ... }, one bundle, one KetoyVM, ViewModels shared across screens
Per-screen opt-outKetoyScreen(entryPoint, useSharedRuntime = false) for argument-scoped screens
DialogsMaterial3 AlertDialog (confirm / info)
LayoutFlowRow (real androidx.compose.foundation.layout.FlowRow)
PagingHorizontalPager / VerticalPager with real snap / fling
DefaultsTextField / OutlinedTextField / Switch .enabled default to true
DINew ketoy-koin module (Koin integration, alongside ketoy-hilt)

App-scoped shared runtime

Until now each KetoyScreen loaded the bundle independently, every route got its own KetoyVM, its own heap, and its own ViewModel instances. Two screens could not share a @KetoyViewModel or any state.

KetoyRuntimeHost fixes that. Wrap your NavHost (or any screen group) in it: it loads and verifies the .ktx once into a single Activity-scoped KetoyVM, and every KetoyScreen inside resolves against that shared runtime.

App.kt
@Composable
fun App() {
  KetoyRuntimeHost(bundleSource = KetoyBundleSource.Asset("ketoy/main.ktx")) {
      val nav = rememberNavController()
      NavHost(nav, startDestination = "home") {
          composable("home")     { KetoyScreen(entryPoint = "HomeScreen") }
          composable("upcoming") { KetoyScreen(entryPoint = "UpcomingScreen") }
          composable("search")   { KetoyScreen(entryPoint = "SearchScreen") }
          composable("details/{id}") {
              // argument-scoped, keep a fresh per-nav-entry ViewModel
              KetoyScreen(entryPoint = "DetailsScreen", useSharedRuntime = false)
          }
      }
  }
}

Now ketoyViewModel<CatalogViewModel>() called from Home, Upcoming, and Search returns the same instance with a single lifecycle. Data one tab fetches in the VM's init is reused by the others, Upcoming appears instantly because CatalogViewModel already loaded its slice for Home.

Shared vs per-screen, the duo

ScopeHowWhen to use
Shared (activity-scoped)Default under KetoyRuntimeHostCross-tab / cross-screen state, a catalog, a session, a cart
Per-screen (per-nav-entry)KetoyScreen(..., useSharedRuntime = false)Argument-scoped screens, a details page keyed by an id

Both coexist. A shared CatalogViewModel for the browse tabs and a per-screen DetailsViewModel keyed by show id is the reference pattern.

What you keep

Native navigation, the real NavHost, back stack, deep links, and transitions are untouched. Real lifecycle, shared VMs live through config change and are cleared with the Activity (real Android ViewModelStore); per-screen VMs are cleared with their nav entry. One runtime, a single parse, heap, and coroutine engine for the whole app instead of one per screen.

No KetoyRuntimeHost? KetoyScreen behaves exactly as in 0.4.x (per-screen runtime). Adopt the host only where you want sharing, a full architecture decision record covers the ComposeEngine-decoupling refactor that makes one shared KetoyVM safe to drive many concurrent screen compositions.

Also shipping (from the 0.4.16-0.4.19 iterations)

  • Material3 AlertDialog, the reusable confirm / info dialog. Nullable content slots (dismissButton / icon / title / text) pass through, so an absent slot renders no button; shape, content colours, and tonalElevation honour-default to AlertDialogDefaults.*. Security-sensitive flows (e.g. a captcha-gated destructive action) can stay native and be triggered from KBC through a host capability.
  • FlowRow, the real androidx.compose.foundation.layout.FlowRow (off the deprecated accompanist wrapper) for wrapping chip / tag rows.
  • Compose Pagers, HorizontalPager / VerticalPager as real KSP-generated catalog adapters, with genuine snap / fling / paging:
Catalog.kt
HorizontalPager(state = rememberPagerState(pageCount = { items.size })) { page ->
  HeroCard(items[page])
}

The pageCount lambda is extracted and re-run reactively, so the page count tracks your backing data. No new opcode or wire change.

  • Sensible input defaults, TextField / OutlinedTextField and Switch now default .enabled to true when omitted (previously false, a disabled control), so a plain text field accepts input without enabled = true.
  • ketoy-koin, a new optional Koin DI integration module, alongside the existing ketoy-hilt.

Fixes rolled in

  • list.size / map.size / array.size reads inside a KBC body no longer crash (NoSuchMethodException: getSize). Reflection now falls back to the Kotlin .size for Collection / Map / Array.
  • COMPOSABLE_CALL freezes the caller's register file, so Register-typed params and captures inside forEach / forEachIndexed bodies see per-iteration values, fixes an IndexOutOfBounds rendering forEach-emitted lists.
  • Collection.isNotEmpty() / Map.isNotEmpty() in a KBC body resolve via a reflection fallback instead of crashing.

Migrating

Nothing to change in existing 0.4.x screens. To adopt cross-screen shared ViewModels, wrap your NavHost in KetoyRuntimeHost(...) and add useSharedRuntime = false to any argument-scoped screen.

Backward compatibleNo wire-format changeMaven Central