Compose multiplatform navigation
Curated agent skills, conventions, and workflows for building Kotlin Multiplatform (KMP) apps with AI coding agents.
npx -y skills add almasumdev/awesome-kotlin-multiplatform-agent-skills --skill compose-multiplatform-navigationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Navigation in Compose Multiplatform with Voyager or Decompose, including deep links, back-handling, and nested graphs. Use when designing multi-screen KMP apps.
SKILL.md
4.9 KB, as published. Nobody here has run it
Compose Multiplatform Navigation
Instructions
androidx.navigation:navigation-compose is Android-only. For CMP, use Voyager (screen-model centric, Compose-first) or Decompose (lifecycle + state-holder centric, better for complex nav / multi-window). Pick one per app.
1. Voyager — minimal, idiomatic Compose
// commonMain/build.gradle.kts
commonMain.dependencies {
implementation(libs.voyager.navigator)
implementation(libs.voyager.screenmodel)
implementation(libs.voyager.transitions)
}
data object HomeScreen : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
Column {
Text("Home")
Button(onClick = { navigator.push(DetailScreen(id = 42)) }) { Text("Open 42") }
}
}
}
data class DetailScreen(val id: Long) : Screen {
@Composable
override fun Content() {
val screenModel = rememberScreenModel { DetailScreenModel(id) }
val state by screenModel.state.collectAsState()
Text("Detail $id: ${state.title}")
}
}
@Composable
fun App() = Navigator(HomeScreen) { nav ->
SlideTransition(nav)
}
Back handling works automatically via BackHandler on Android and via the Compose iOS runtime's system back. For deep links, resolve the URL at your entry point and navigator.replaceAll(...) the target screen:
fun handleDeepLink(url: String, nav: Navigator) {
val uri = Url(url)
when (uri.pathSegments.firstOrNull()) {
"item" -> nav.replaceAll(listOf(HomeScreen, DetailScreen(uri.pathSegments[1].toLong())))
}
}
2. Decompose — component tree, great for tablet / master-detail
sealed interface RootComponent {
val stack: Value<ChildStack<*, Child>>
fun onBackClicked()
sealed interface Child {
class Home(val component: HomeComponent) : Child
class Detail(val component: DetailComponent) : Child
}
}
class DefaultRootComponent(componentContext: ComponentContext) :
RootComponent, ComponentContext by componentContext {
private val nav = StackNavigation<Config>()
override val stack: Value<ChildStack<*, RootComponent.Child>> = childStack(
source = nav,
serializer = Config.serializer(),
initialConfiguration = Config.Home,
handleBackButton = true,
) { config, ctx ->
when (config) {
Config.Home -> RootComponent.Child.Home(DefaultHomeComponent(ctx, ::openDetail))
is Config.Detail -> RootComponent.Child.Detail(DefaultDetailComponent(ctx, config.id))
}
}
private fun openDetail(id: Long) = nav.pushNew(Config.Detail(id))
override fun onBackClicked() = nav.pop()
@Serializable
private sealed interface Config {
@Serializable data object Home : Config
@Serializable data class Detail(val id: Long) : Config
}
}
@Composable
fun RootUi(component: RootComponent) {
Children(stack = component.stack, animation = stackAnimation(slide())) {
when (val child = it.instance) {
is RootComponent.Child.Home -> HomeUi(child.component)
is RootComponent.Child.Detail -> DetailUi(child.component)
}
}
}
Decompose integrates with Android SavedStateHandle, iOS lifecycle (via LifecycleRegistry wired in the iOS entry point), and produces a serializable back stack — robust against process death.
3. Deep links
Centralize URL parsing in commonMain. Emit typed intents that navigation code consumes:
sealed interface DeepLink {
data object Home : DeepLink
data class Item(val id: Long) : DeepLink
}
fun parseDeepLink(raw: String): DeepLink? = runCatching {
val url = Url(raw)
when (url.pathSegments.firstOrNull()) {
"item" -> DeepLink.Item(url.pathSegments[1].toLong())
else -> DeepLink.Home
}
}.getOrNull()
Android consumes via Activity.onNewIntent; iOS via SceneDelegate/@main App.
4. Back press & gesture-back
- Android: the navigator library registers a
BackHandler; don't register your own globally. - iOS: Compose MP 1.7+ honours interactive pop gestures automatically when a
Navigator/Decompose stack is non-empty. - Desktop: wire your own keyboard shortcut (e.g.
Esc) usingonPreviewKeyEvent.
Checklist
- Exactly one navigation library in use — no mixing
navigation-composeinto a CMP app. - Screen models (Voyager) or components (Decompose) live in
commonMain. - Deep-link parsing returns typed results, no raw strings leaking into UI.
- Back stack survives process death (Decompose
serializerset; VoyagerrememberScreenwith stable keys). - iOS back-swipe and Android hardware back both work without custom code.