This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Decompose-Router is a Compose Multiplatform navigation library that wraps Decompose in a Compose-first, Conductor-inspired API. Targets: Android, WearOS, Desktop (JVM), iOS, JS (browser), and Wasm/JS.
Group/coordinates: io.github.xxfast. Published modules: decompose-router and decompose-router-wear.
decompose-router— the library. MultiplatformcommonMainholds the public API; per-platform source sets (androidMain,iosMain,desktopMain,jsMain,wasmJsMain) provideDefaultRouterContextandKeyactuals.decompose-router-wear— Android-only WearOS overlay built on top ofdecompose-router(depends on it viaapi(project(":decompose-router"))). Has a phantomjvm()target only as a Dokka workaround.app— internal demo/integration-test app across all targets. Not published (excluded from publishing and API validation). Holds the Android instrumented tests that actually exercise the routers.sample— a git submodule pointing at the separate NYTimes-KMP repo. Not part of this build.
Run via the Gradle wrapper (./gradlew). JDK 17+ (CI uses 21).
# API binary-compatibility check — run before pushing public-API changes
./gradlew apiCheck
# Regenerate the .api dumps after an intentional public API change
./gradlew apiDump
# Per-target test suites (mirrors CI matrix)
./gradlew testAndroidHostTest # Android host (JVM) unit tests
./gradlew iosSimulatorArm64Test # iOS simulator (macOS only)
./gradlew jsTest # JS browser
./gradlew desktopTest # Desktop JVM
./gradlew connectedAndroidDeviceTest # Android instrumented tests (needs emulator/device)
# Run the demo app
./gradlew :app:run # Desktop
./gradlew :app:jsBrowserRun # JS in browser
./gradlew :app:wasmJsBrowserRun # Wasm in browser
# Android: install :app on a device/emulator; iOS: open app/ios/ios.xcodeproj in Xcode
# Documentation
./gradlew dokkaGenerate # API reference (HTML) into build/dokka/htmlThere is no separate lint step; apiCheck (binary-compatibility-validator) is the gating static check in CI alongside the per-target test tasks.
Most of the real behavior is integration-tested in app, not unit-tested in the library. When changing router semantics, the tests to run/update are the Android instrumented tests under app/src/androidDeviceTest/.../app/ (TestStackRouter, TestSlotRouters, TestPagesRouters, TestNestedRouters). These require an emulator (connectedAndroidDeviceTest) and only run on Android in CI. commonTest in the library is minimal.
The library is intentionally small — roughly a hundred lines of core glue over Decompose. Understanding these pieces is enough to work anywhere in it.
RouterContext (commonMain/.../RouterContext.kt) — the central type. It wraps Decompose's ComponentContext (via delegation) and adds a storage map for retaining router instances. Each route/screen gets its own RouterContext. It is passed down the Compose tree through the LocalRouterContext composition local. The root must be provided by the host platform: on Android via ComponentActivity.defaultRouterContext() provided into LocalRouterContext in MainActivity; other platforms have their own defaultRouterContext() actuals in their source sets.
Three router flavors, each a folder under commonMain/.../router/ with a Router.kt (the rememberRouter* factory + Router class) and a RoutedContent.kt (the Composable that renders the current child(ren)):
stack/— a back stack (push/pop);RoutedContentrenders the top.slot/— zero-or-one active child (e.g. dialogs/overlays).pages/— a pager of children.
Each Router<C> wraps a Decompose *Navigation<C> (delegating navigation calls to it) plus a Compose State of the current Decompose child collection. Value.asState() (State.kt) bridges Decompose's Value<T> to Compose State<T>, subscribed to the lifecycle.
Instance retention — two distinct mechanisms, don't confuse them:
rememberOnRoute(key) { ... }(RememberOnRoute.kt) scopes an arbitrary instance (view model, state holder) to a route using Decompose'sInstanceKeeper. It survives configuration changes and is cleared when the route leaves the back stack. TheRouterContextitself is what makes this work per-route.RouterContext.state(initial, ...) { supplier }(RouterContext.kt) survives process death by registering a@Serializablevalue with Decompose'sStateKeeper. Use this for state that must outlive the process, not just config changes.
Configurations are the navigation keys: user-defined @Serializable sealed types. Serialization is how Decompose persists the stack, hence the kotlinx.serialization requirement on configs.
API surface conventions: public rememberRouter / rememberOnRoute come in an inline reified form (preferred) and a @Deprecated KClass-based form kept for migration — keep both in sync when changing signatures, and run apiDump for any public change.
- Versions are centralized in
gradle/libs.versions.toml. Current version is set in the rootbuild.gradle.kts(allprojects { version = ... }). - Publishing uses the
com.vanniktech.maven.publishplugin, applied to every published subproject from the rootbuild.gradle.ktssubprojects {}block (theappis skipped). Two registries: Maven Central viapublishToMavenCentral(Sonatype Central Portal,SNAPSHOTs go to the snapshots repo; releases wait for manual "Publish" on the portal since there's noautomaticRelease) and GitHub Packages viapublishAllPublicationsToGitHubPackagesRepository. Artifacts are GPG-signed (signAllPublications()) and the plugin wires the Dokka javadoc jar automatically. - Credentials are Gradle properties, not
local.properties:mavenCentralUsername/mavenCentralPasswordandsigningInMemoryKey/signingInMemoryKeyPasswordfor Central;GITHUB_ACTOR/GITHUB_TOKENenv vars for GitHub Packages. In CI these come fromORG_GRADLE_PROJECT_*env vars fed by repo secrets (MAVEN_CENTRAL_USERNAME/MAVEN_CENTRAL_PASSWORD,GPG_KEY_SECRET/GPG_KEY_PASSWORD). - CI (
.github/workflows/build.yml) runsapiCheck→ per-target tests + Android emulator tests → (on push tomain) areleasematrix job publishing to both registries and a docs job publishing to GitHub Pages.