Router & navigation
Navigation is core-adjacent by design: BackStack lives in the runtime, and the router battery (kinetica-router) adds the host component, transitions, history and deep links.
Routes and the back stack
@Serializable
sealed interface AppRoute : Route {
@Serializable data object Home : AppRoute
@Serializable data class Details(val id: String) : AppRoute
}
val stack = BackStack<AppRoute>(AppRoute.Home)
stack.push(AppRoute.Details("42"))
stack.pop() // false when only the root remains
stack.replaceAll(AppRoute.Home) // resetBackStack is itself a reactive cell over List<Route> — reading it in render subscribes, and it enforces the invariant that it is never empty.
NavHost
NavHost(stack, options = NavOptions(retainPreviousEntries = 1, restoreScroll = true)) { route ->
when (route) {
AppRoute.Home -> HomeScreen()
is AppRoute.Details -> DetailsScreen(route.id)
}
}NavHost renders the current route and up to retainPreviousEntries previous entries (kept mounted, marked retained — the entry you'd return to keeps its state). Dropped entries have their non-persistent state disposed. Each entry knows its NavEntry — direction (NavDirection: Initial, Unchanged, Forward, Back, Replace), transition, stack index — via currentNavEntry().
Transitions annotate entries for renderers: NavOptions(transition = NavTransition.slide(200)) (None, fade(ms), slide(ms)).
Scroll restoration
val scroll = rememberNavScrollState(key = "feed")
navLazyEach(items, key = { it.id }, scrollKey = "feed") { item -> Row(item) }Scroll state persists per navigation entry: navigate away and back, the window position is restored (restoreScroll = true).
System back
val dispatcher = InMemoryHostBackDispatcher() // adapt to browser/Android back
val binding = stack.bindHostBack(dispatcher) // back -> stack.pop()
dispatcher.dispatchBack() // true when handledHandlers are dispatched newest-first; the first to return true wins — register your own (dispatcher.registerBackHandler { … }) to intercept back before the stack pops (e.g. to close a dialog).
Serialization, history, deep links
val codec = jsonRouteCodec(AppRoute.serializer()) // any RouteCodec<R>
val history = InMemoryRouteHistory() // adapt to browser History API
stack.writeToHistory(history, codec) // push or replace
stack.restoreFromHistory(history, codec)
val link = codec.deepLink(AppRoute.Details("42")) // encoded deep link
val parsed = codec.parseDeepLink(link.encoded) // DeepLink<AppRoute>; the route is parsed.routeBecause routes are serializable values, the whole stack round-trips through history storage — state restoration after process death is the same code path as browser history.