agentsclimarketplace

Cmp navigation

Skill ShinKev/kmp-skill-library/ui/cmp-navigation

19 Claude AI skills for Kotlin Multiplatform (KMP) & Compose Multiplatform (CMP): architecture, networking, UI, testing, migration & build tooling for Android and iOS.

Install
npx -y skills add ShinKev/kmp-skill-library --skill cmp-navigation

Assembled 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

Implement type-safe navigation in Compose Multiplatform using the JetBrains Navigation Compose Multiplatform library. Use when setting up navigation, passing arguments, handling deep links, or structuring multi-screen KMP apps.

SKILL.md

7.2 KB, as published. Nobody here has run it

CMP Navigation (JetBrains Navigation Compose Multiplatform)

Overview

Implement type-safe navigation in Compose Multiplatform applications using the JetBrains fork of Navigation Compose. The API is nearly identical to AndroidX Navigation Compose, making migration straightforward. The NavHost and route definitions live in commonMain.

Setup

// libs.versions.toml
[libraries]
navigation-compose = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version = "2.8.0-alpha10" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }

[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
// shared/build.gradle.kts
plugins {
    alias(libs.plugins.kotlin.serialization)
}
commonMain.dependencies {
    implementation(libs.navigation.compose)
    implementation(libs.kotlinx.serialization.json)
}

Core Concepts

1. Define Routes (Type-Safe, in commonMain)

import kotlinx.serialization.Serializable

@Serializable object Home
@Serializable data class Profile(val userId: String)
@Serializable data class Settings(val section: String? = null)
@Serializable object AuthGraph
@Serializable object Login

2. Create NavHost (in commonMain)

@Composable
fun AppNavHost(
    navController: NavHostController = rememberNavController(),
    modifier: Modifier = Modifier
) {
    NavHost(
        navController = navController,
        startDestination = Home,
        modifier = modifier
    ) {
        composable<Home> {
            HomeScreen(onNavigateToProfile = { userId ->
                navController.navigate(Profile(userId))
            })
        }

        composable<Profile> { backStackEntry ->
            val profile: Profile = backStackEntry.toRoute()
            ProfileScreen(userId = profile.userId)
        }

        composable<Settings> { backStackEntry ->
            val settings: Settings = backStackEntry.toRoute()
            SettingsScreen(section = settings.section)
        }

        navigation<AuthGraph>(startDestination = Login) {
            composable<Login> {
                LoginScreen(onLoginSuccess = {
                    navController.navigate(Home) {
                        popUpTo<AuthGraph> { inclusive = true }
                    }
                })
            }
        }
    }
}

Navigation Patterns

Basic Navigation

navController.navigate(Profile(userId = "user123"))
navController.popBackStack()
navController.popBackStack<Home>(inclusive = false)

Navigate with Options

navController.navigate(Home) {
    popUpTo<Home> { inclusive = true; saveState = true }
    launchSingleTop = true
    restoreState = true
}

Bottom Navigation (Adaptive)

@Composable
fun MainScreen() {
    val navController = rememberNavController()

    // NavigationSuiteScaffold automatically switches between bottom bar / rail / drawer
    NavigationSuiteScaffold(
        navigationSuiteItems = {
            val current = navController.currentBackStackEntryAsState().value?.destination
            item(
                icon = { Icon(Icons.Default.Home, null) },
                label = { Text("Home") },
                selected = current?.hasRoute<Home>() == true,
                onClick = {
                    navController.navigate(Home) {
                        popUpTo(navController.graph.findStartDestination().id) { saveState = true }
                        launchSingleTop = true
                        restoreState = true
                    }
                }
            )
        }
    ) {
        AppNavHost(navController = navController)
    }
}

ViewModel with Navigation Arguments

Default pattern: read route arguments from SavedStateHandle. This survives process death, integrates naturally with deep links, and keeps the composable signature clean. The VM is reconstructed automatically with its original args after backgrounding.

// commonMain — no annotations
class ProfileViewModel(
    savedStateHandle: SavedStateHandle,
    private val userRepository: UserRepository
) : ViewModel() {

    private val profile: Profile = savedStateHandle.toRoute<Profile>()

    val user: StateFlow<User?> = userRepository
        .getUser(profile.userId)
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
}
// Koin module — SavedStateHandle is provided automatically by koin-compose-viewmodel
viewModel { ProfileViewModel(get(), get()) }

// Composable — no params needed; userId comes from the route
composable<Profile> {
    val viewModel: ProfileViewModel = koinViewModel()
    ProfileScreen(viewModel)
}

When to use parametersOf instead

For VMs that are not tied to a navigation destination (a dialog VM, a list-item VM, an embedded composable), pass primitives via Koin's parameters:

// VM has no SavedStateHandle dependency
class ItemRowViewModel(
    private val itemId: String,
    private val repo: ItemRepository
) : ViewModel()

// Koin
viewModel { params -> ItemRowViewModel(params.get(), get()) }

// Composable
@Composable
fun ItemRow(itemId: String) {
    val vm: ItemRowViewModel = koinViewModel(parameters = { parametersOf(itemId) })
}

Do not mix the two patterns on the same VM — pick one. Prefer SavedStateHandle for any VM scoped to a NavBackStackEntry.


Deep Links

Deep link URI patterns are defined in commonMain. Manifest <intent-filter> goes in androidApp.

// commonMain
composable<Profile>(
    deepLinks = listOf(
        navDeepLink<Profile>(basePath = "https://example.com/profile")
    )
) { backStackEntry ->
    ProfileScreen(userId = backStackEntry.toRoute<Profile>().userId)
}
<!-- androidApp/src/main/AndroidManifest.xml — Android-specific -->
<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" android:host="example.com" />
    </intent-filter>
</activity>

Critical Rules

DO

  • Use @Serializable routes for type safety
  • Pass only primitive IDs as navigation arguments, fetch full objects in ViewModel
  • Define NavHost in commonMain
  • Place manifest <intent-filter> in androidApp only

DON'T

  • Pass complex objects as navigation arguments
  • Use hiltViewModel() — use koinViewModel() instead
  • Use string-based routes (legacy pattern)
  • Put Android-specific deep link setup in commonMain

References

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.