
ViewModel-driven navigation architecture enabling sealed scenes, NavigationState actions (push/pop/present), SceneRepository for passing data, plus declarative UI integration and modal-stack manager.
A Kotlin Multiplatform navigation architecture library providing shared ViewModel-driven navigation for iOS and Android.
KMP module (commonMain + androidMain + iosMain) containing:
AppScene — Marker interface for navigation destinationsNavigationState — Sealed interface for navigation actions (Push, Pop, PresentSheet, etc.)NavigationViewModel — ViewModel base class with navigation support (extends architecture kit's BaseViewModel)SceneRepository — Pattern for passing data between scenesDepends on fosh-labs-kmp-architecture-kit for BaseViewModel, UseCase, and ViewModelState.
Android-only module with Jetpack Compose navigation integration:
HandleNavigation — Composable that observes ViewModel navigation eventsNavigationManager — Modal stack tracking for AndroidThe library is published to Maven Central. Add the dependency to your project:
Option 1: Kotlin DSL (build.gradle.kts)
// In your shared KMP module's build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
api("io.github.foshlabs.kmp.navigationkit:navigation-core:0.1.2")
}
androidMain.dependencies {
implementation("io.github.foshlabs.kmp.navigationkit:navigation-compose:0.1.2")
}
}
}
// For iOS: add export("io.github.foshlabs.kmp.navigationkit:navigation-core:0.1.2") to your framework blockOption 2: Version catalog (libs.versions.toml)
[versions]
foshlabs-navigation = "0.1.2"
[libraries]
foshlabs-navigation-core = { group = "io.github.foshlabs.kmp.navigationkit", name = "navigation-core", version.ref = "foshlabs-navigation" }
foshlabs-navigation-compose = { group = "io.github.foshlabs.kmp.navigationkit", name = "navigation-compose", version.ref = "foshlabs-navigation" }// In your build.gradle.kts
commonMain.dependencies {
api(libs.foshlabs.navigation.core)
}
androidMain.dependencies {
implementation(libs.foshlabs.navigation.compose)
}Maven Central is included by default in most Gradle projects. If needed, ensure
mavenCentral()is in yourrepositoriesblock.
In your project's shared module, create a sealed interface extending AppScene:
import io.github.foshlabs.kmp.navigationkit.AppScene
sealed interface MyProjectScene : AppScene {
data object Home : MyProjectScene
data object Settings : MyProjectScene
data object Onboarding : MyProjectScene
data object Paywall : MyProjectScene
}ViewModels that need navigation extend NavigationViewModel and call navigate() with NavigationState actions:
import io.github.foshlabs.kmp.navigationkit.NavigationViewModel
import io.github.foshlabs.kmp.navigationkit.NavigationState
import io.github.foshlabs.kmp.architecturekit.ViewModelState
class HomeViewModel : NavigationViewModel<HomeViewModel.State>(State()) {
data class State(
val title: String = "Home"
) : ViewModelState
fun onTapSettings() {
navigate(NavigationState.Push(MyProjectScene.Settings))
}
fun onTapPaywall() {
navigate(NavigationState.PresentSheet(MyProjectScene.Paywall))
}
}Available navigation actions:
NavigationState.Push(destination) — Push onto the navigation stackNavigationState.Pop — Pop the current screenNavigationState.PopToRoot — Pop to the root of the current contextNavigationState.PopTo(destination, inclusive) — Pop to a specific destinationNavigationState.PresentSheet(destination) — Present as a sheet/modalNavigationState.PresentFullScreen(destination) — Present as a full-screen modalNavigationState.Dismiss — Dismiss the current modal (pops entire modal stack)NavigationState.ReplaceRoot(destination) — Replace the entire navigation stackCreate @Serializable route objects and a mapper function:
import io.github.foshlabs.kmp.navigationkit.AppScene
import kotlinx.serialization.Serializable
@Serializable object HomeRoute
@Serializable object SettingsRoute
@Serializable object OnboardingRoute
@Serializable object PaywallRoute
fun mapToDestination(scene: AppScene): Any {
val myScene = scene as MyProjectScene
return when (myScene) {
MyProjectScene.Home -> HomeRoute
MyProjectScene.Settings -> SettingsRoute
MyProjectScene.Onboarding -> OnboardingRoute
MyProjectScene.Paywall -> PaywallRoute
}
}Wrap the library's HandleNavigation with your scene mapper so call sites stay clean:
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import io.github.foshlabs.kmp.navigationkit.NavigationViewModel
import io.github.foshlabs.kmp.architecturekit.ViewModelState
import io.github.foshlabs.kmp.navigationkit.compose.HandleNavigation as LibraryHandleNavigation
@Composable
fun <S : ViewModelState> HandleNavigation(
viewModel: NavigationViewModel<S>,
navController: NavController
) {
LibraryHandleNavigation(
viewModel = viewModel,
navController = navController,
sceneMapper = { scene -> mapToDestination(scene) }
)
}Provide NavigationManager via CompositionLocalProvider and use HandleNavigation in each screen:
import io.github.foshlabs.kmp.navigationkit.compose.LocalNavigationManager
import io.github.foshlabs.kmp.navigationkit.compose.NavigationManager
@Composable
fun App() {
val navController = rememberNavController()
CompositionLocalProvider(LocalNavigationManager provides NavigationManager()) {
NavHost(navController = navController, startDestination = HomeRoute) {
composable<HomeRoute> {
val viewModel: HomeViewModel = koinViewModel()
HandleNavigation(viewModel, navController)
HomeScreen(viewModel)
}
composable<SettingsRoute> {
val viewModel: SettingsViewModel = koinViewModel()
HandleNavigation(viewModel, navController)
SettingsScreen(viewModel)
}
// ... more destinations
}
}
}See fosh-labs-kmp-navigation-kit-ios for the SwiftUI navigation components.
| Dependency | Version |
|---|---|
| Kotlin | 2.2.0 |
| Fosh Labs Architecture Kit | 0.1.0 |
| KMP Observable ViewModel | 1.0.0-BETA-7 |
| KMP Native Coroutines | 1.0.0-ALPHA-45 |
| Koin | 4.0.0 |
| AndroidX Navigation Compose | 2.8.3 |
Publishing to Maven Central is configured. Credentials are loaded from gradle-secrets.properties (gitignored).
Important: The vanniktech plugin reads mavenCentralUsername/mavenCentralPassword via Gradle properties (not from ext). Use the wrapper script:
./publishToMavenCentral.shThis reads ossrhUsername/ossrhPassword from gradle-secrets.properties and passes them to Gradle. Alternatively, run with -PmavenCentralUsername=… -PmavenCentralPassword=… or set ORG_GRADLE_PROJECT_mavenCentralUsername and ORG_GRADLE_PROJECT_mavenCentralPassword env vars.
A Kotlin Multiplatform navigation architecture library providing shared ViewModel-driven navigation for iOS and Android.
KMP module (commonMain + androidMain + iosMain) containing:
AppScene — Marker interface for navigation destinationsNavigationState — Sealed interface for navigation actions (Push, Pop, PresentSheet, etc.)NavigationViewModel — ViewModel base class with navigation support (extends architecture kit's BaseViewModel)SceneRepository — Pattern for passing data between scenesDepends on fosh-labs-kmp-architecture-kit for BaseViewModel, UseCase, and ViewModelState.
Android-only module with Jetpack Compose navigation integration:
HandleNavigation — Composable that observes ViewModel navigation eventsNavigationManager — Modal stack tracking for AndroidThe library is published to Maven Central. Add the dependency to your project:
Option 1: Kotlin DSL (build.gradle.kts)
// In your shared KMP module's build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
api("io.github.foshlabs.kmp.navigationkit:navigation-core:0.1.2")
}
androidMain.dependencies {
implementation("io.github.foshlabs.kmp.navigationkit:navigation-compose:0.1.2")
}
}
}
// For iOS: add export("io.github.foshlabs.kmp.navigationkit:navigation-core:0.1.2") to your framework blockOption 2: Version catalog (libs.versions.toml)
[versions]
foshlabs-navigation = "0.1.2"
[libraries]
foshlabs-navigation-core = { group = "io.github.foshlabs.kmp.navigationkit", name = "navigation-core", version.ref = "foshlabs-navigation" }
foshlabs-navigation-compose = { group = "io.github.foshlabs.kmp.navigationkit", name = "navigation-compose", version.ref = "foshlabs-navigation" }// In your build.gradle.kts
commonMain.dependencies {
api(libs.foshlabs.navigation.core)
}
androidMain.dependencies {
implementation(libs.foshlabs.navigation.compose)
}Maven Central is included by default in most Gradle projects. If needed, ensure
mavenCentral()is in yourrepositoriesblock.
In your project's shared module, create a sealed interface extending AppScene:
import io.github.foshlabs.kmp.navigationkit.AppScene
sealed interface MyProjectScene : AppScene {
data object Home : MyProjectScene
data object Settings : MyProjectScene
data object Onboarding : MyProjectScene
data object Paywall : MyProjectScene
}ViewModels that need navigation extend NavigationViewModel and call navigate() with NavigationState actions:
import io.github.foshlabs.kmp.navigationkit.NavigationViewModel
import io.github.foshlabs.kmp.navigationkit.NavigationState
import io.github.foshlabs.kmp.architecturekit.ViewModelState
class HomeViewModel : NavigationViewModel<HomeViewModel.State>(State()) {
data class State(
val title: String = "Home"
) : ViewModelState
fun onTapSettings() {
navigate(NavigationState.Push(MyProjectScene.Settings))
}
fun onTapPaywall() {
navigate(NavigationState.PresentSheet(MyProjectScene.Paywall))
}
}Available navigation actions:
NavigationState.Push(destination) — Push onto the navigation stackNavigationState.Pop — Pop the current screenNavigationState.PopToRoot — Pop to the root of the current contextNavigationState.PopTo(destination, inclusive) — Pop to a specific destinationNavigationState.PresentSheet(destination) — Present as a sheet/modalNavigationState.PresentFullScreen(destination) — Present as a full-screen modalNavigationState.Dismiss — Dismiss the current modal (pops entire modal stack)NavigationState.ReplaceRoot(destination) — Replace the entire navigation stackCreate @Serializable route objects and a mapper function:
import io.github.foshlabs.kmp.navigationkit.AppScene
import kotlinx.serialization.Serializable
@Serializable object HomeRoute
@Serializable object SettingsRoute
@Serializable object OnboardingRoute
@Serializable object PaywallRoute
fun mapToDestination(scene: AppScene): Any {
val myScene = scene as MyProjectScene
return when (myScene) {
MyProjectScene.Home -> HomeRoute
MyProjectScene.Settings -> SettingsRoute
MyProjectScene.Onboarding -> OnboardingRoute
MyProjectScene.Paywall -> PaywallRoute
}
}Wrap the library's HandleNavigation with your scene mapper so call sites stay clean:
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import io.github.foshlabs.kmp.navigationkit.NavigationViewModel
import io.github.foshlabs.kmp.architecturekit.ViewModelState
import io.github.foshlabs.kmp.navigationkit.compose.HandleNavigation as LibraryHandleNavigation
@Composable
fun <S : ViewModelState> HandleNavigation(
viewModel: NavigationViewModel<S>,
navController: NavController
) {
LibraryHandleNavigation(
viewModel = viewModel,
navController = navController,
sceneMapper = { scene -> mapToDestination(scene) }
)
}Provide NavigationManager via CompositionLocalProvider and use HandleNavigation in each screen:
import io.github.foshlabs.kmp.navigationkit.compose.LocalNavigationManager
import io.github.foshlabs.kmp.navigationkit.compose.NavigationManager
@Composable
fun App() {
val navController = rememberNavController()
CompositionLocalProvider(LocalNavigationManager provides NavigationManager()) {
NavHost(navController = navController, startDestination = HomeRoute) {
composable<HomeRoute> {
val viewModel: HomeViewModel = koinViewModel()
HandleNavigation(viewModel, navController)
HomeScreen(viewModel)
}
composable<SettingsRoute> {
val viewModel: SettingsViewModel = koinViewModel()
HandleNavigation(viewModel, navController)
SettingsScreen(viewModel)
}
// ... more destinations
}
}
}See fosh-labs-kmp-navigation-kit-ios for the SwiftUI navigation components.
| Dependency | Version |
|---|---|
| Kotlin | 2.2.0 |
| Fosh Labs Architecture Kit | 0.1.0 |
| KMP Observable ViewModel | 1.0.0-BETA-7 |
| KMP Native Coroutines | 1.0.0-ALPHA-45 |
| Koin | 4.0.0 |
| AndroidX Navigation Compose | 2.8.3 |
Publishing to Maven Central is configured. Credentials are loaded from gradle-secrets.properties (gitignored).
Important: The vanniktech plugin reads mavenCentralUsername/mavenCentralPassword via Gradle properties (not from ext). Use the wrapper script:
./publishToMavenCentral.shThis reads ossrhUsername/ossrhPassword from gradle-secrets.properties and passes them to Gradle. Alternatively, run with -PmavenCentralUsername=… -PmavenCentralPassword=… or set ORG_GRADLE_PROJECT_mavenCentralUsername and ORG_GRADLE_PROJECT_mavenCentralPassword env vars.