Sitemap
Haider Ali Khan

Haider Ali Khan

Android & AI/ML Engineer. Native Android, Compose Multiplatform, scalable apps, clean code, and intelligent systems

Navigation in Kotlin Multiplatform: A Complete Guide for Modern Developers

6 min readJun 1, 2025

--

Press enter or click to view image in full size

The landscape of mobile development has evolved dramatically, and Kotlin Multiplatform (KMP) has emerged as a game-changing technology that allows developers to share code across multiple platforms. While Jetpack Navigation 3 has been announced for Android, the multiplatform navigation story requires a different approach. This comprehensive guide explores how to implement robust navigation in your Kotlin Multiplatform projects using current, stable technologies.

Understanding Navigation in Kotlin Multiplatform

Navigation in multiplatform projects presents unique challenges compared to single-platform development. You need to consider platform-specific navigation patterns while maximizing code sharing opportunities. The key is to abstract navigation logic into common code while allowing platform-specific implementations where necessary.

Current State of Multiplatform Navigation

As of 2025, the navigation landscape for Kotlin Multiplatform includes:

  • Compose Multiplatform Navigation: The official navigation library from JetBrains
  • Custom navigation solutions: Platform-agnostic routing systems
  • Hybrid approaches: Combining shared logic with platform-specific implementations

Setting Up Navigation in Kotlin Multiplatform

Project Configuration

Begin by adding the necessary dependencies to your multiplatform project. In your build.gradle.kts file:

kotlin {
sourceSets {
val commonMain by getting {
dependencies {
// Compose Multiplatform Navigation (currently in Beta)
implementation("org.jetbrains.androidx.navigation:navigation-compose:2.9.0-beta02")

// For shared ViewModels
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2")
}
}

val androidMain by getting {
dependencies {
// Android-specific navigation if needed
implementation("androidx.navigation:navigation-compose:2.8.4")
implementation("androidx.activity:activity-compose:1.9.3")
}
}

val iosMain by getting {
dependencies {
// iOS-specific dependencies if needed
}
}
}
}

Defining Navigation Structure

Create a shared navigation structure that can be utilized across platforms:

// commonMain/navigation/NavigationDestinations.kt
sealed class Screen(val route: String) {
data object Home : Screen("home")
data object Settings : Screen("settings")

data class Profile(val userId: String = "{userId}") : Screen("profile/$userId") {
fun createRoute(userId: String) = "profile/$userId"

companion object {
const val ROUTE_WITH_ARGS = "profile/{userId}"
}
}

data class ProductDetail(val productId: String = "{productId}") : Screen("product/$productId") {
fun createRoute(productId: String) = "product/$productId"

companion object {
const val ROUTE_WITH_ARGS = "product/{productId}"
}
}
}
// Navigation arguments
object NavigationArgs {
const val USER_ID = "userId"
const val PRODUCT_ID = "productId"
}

Common Navigation Logic

// commonMain/navigation/NavigationViewModel.kt
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.*
class NavigationViewModel : ViewModel() {
private val _navigationState = MutableStateFlow<NavigationState>(NavigationState.Idle)
val navigationState = _navigationState.asStateFlow()

private val _canNavigateBack = MutableStateFlow(false)
val canNavigateBack = _canNavigateBack.asStateFlow()

fun navigateTo(destination: String) {
_navigationState.value = NavigationState.Navigate(destination)
}

fun navigateBack() {
_navigationState.value = NavigationState.NavigateBack
}

fun clearNavigation() {
_navigationState.value = NavigationState.Idle
}

fun updateBackStackState(canGoBack: Boolean) {
_canNavigateBack.value = canGoBack
}
}
sealed class NavigationState {
data object Idle : NavigationState()
data class Navigate(val destination: String) : NavigationState()
data object NavigateBack : NavigationState()
}

Platform-Specific Implementation

Android Implementation

// androidMain/navigation/AndroidNavigation.kt
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavHostController
import androidx.navigation.compose.*
import androidx.navigation.navArgument
import androidx.navigation.NavType
@Composable
actual fun AppNavigation(navigationViewModel: NavigationViewModel) {
val navController = rememberNavController()
val navigationState by navigationViewModel.navigationState.collectAsState()

// Handle navigation events
NavigationEventHandler(
navigationState = navigationState,
navController = navController,
onEventHandled = navigationViewModel::clearNavigation,
onBackStackChanged = navigationViewModel::updateBackStackState
)

NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
composable(Screen.Home.route) {
HomeScreen(
onNavigateToProfile = { userId ->
navigationViewModel.navigateTo(Screen.Profile().createRoute(userId))
},
onNavigateToSettings = {
navigationViewModel.navigateTo(Screen.Settings.route)
}
)
}

composable(
route = Screen.Profile.ROUTE_WITH_ARGS,
arguments = listOf(
navArgument(NavigationArgs.USER_ID) {
type = NavType.StringType
}
)
) { backStackEntry ->
val userId = backStackEntry.arguments?.getString(NavigationArgs.USER_ID) ?: ""
ProfileScreen(
userId = userId,
onNavigateBack = navigationViewModel::navigateBack
)
}

composable(Screen.Settings.route) {
SettingsScreen(
onNavigateBack = navigationViewModel::navigateBack
)
}

composable(
route = Screen.ProductDetail.ROUTE_WITH_ARGS,
arguments = listOf(
navArgument(NavigationArgs.PRODUCT_ID) {
type = NavType.StringType
}
)
) { backStackEntry ->
val productId = backStackEntry.arguments?.getString(NavigationArgs.PRODUCT_ID) ?: ""
ProductDetailScreen(
productId = productId,
onNavigateBack = navigationViewModel::navigateBack
)
}
}
}
@Composable
private fun NavigationEventHandler(
navigationState: NavigationState,
navController: NavHostController,
onEventHandled: () -> Unit,
onBackStackChanged: (Boolean) -> Unit
) {
// Monitor back stack changes
val backStackEntry by navController.currentBackStackEntryAsState()

LaunchedEffect(backStackEntry) {
val canGoBack = navController.previousBackStackEntry != null
onBackStackChanged(canGoBack)
}

// Handle navigation events
LaunchedEffect(navigationState) {
when (navigationState) {
is NavigationState.Navigate -> {
try {
navController.navigate(navigationState.destination) {
// Configure navigation options
launchSingleTop = true
restoreState = true
}
} catch (e: Exception) {
// Handle navigation errors
println("Navigation error: ${e.message}")
}
onEventHandled()
}
is NavigationState.NavigateBack -> {
if (navController.previousBackStackEntry != null) {
navController.popBackStack()
}
onEventHandled()
}
NavigationState.Idle -> {
// Do nothing
}
}
}
}

iOS Implementation (Conceptual)

// iosMain/navigation/IOSNavigation.kt
import androidx.compose.runtime.*
@Composable
actual fun AppNavigation(navigationViewModel: NavigationViewModel) {
val navigationState by navigationViewModel.navigationState.collectAsState()
var currentScreen by remember { mutableStateOf<Screen>(Screen.Home) }

// Handle navigation state changes
LaunchedEffect(navigationState) {
when (navigationState) {
is NavigationState.Navigate -> {
// Parse destination and update current screen
currentScreen = parseDestination(navigationState.destination)
navigationViewModel.clearNavigation()
}
is NavigationState.NavigateBack -> {
// Handle back navigation
currentScreen = Screen.Home // Simplified
navigationViewModel.clearNavigation()
}
NavigationState.Idle -> {
// Do nothing
}
}
}

// Render current screen
when (currentScreen) {
is Screen.Home -> {
HomeScreen(
onNavigateToProfile = { userId ->
navigationViewModel.navigateTo(Screen.Profile().createRoute(userId))
},
onNavigateToSettings = {
navigationViewModel.navigateTo(Screen.Settings.route)
}
)
}
is Screen.Profile -> {
ProfileScreen(
userId = extractUserId(currentScreen.route),
onNavigateBack = navigationViewModel::navigateBack
)
}
is Screen.Settings -> {
SettingsScreen(
onNavigateBack = navigationViewModel::navigateBack
)
}
}
}
private fun parseDestination(destination: String): Screen {
return when {
destination == Screen.Home.route -> Screen.Home
destination == Screen.Settings.route -> Screen.Settings
destination.startsWith("profile/") -> {
val userId = destination.substringAfter("profile/")
Screen.Profile(userId)
}
else -> Screen.Home
}
}
private fun extractUserId(route: String): String {
return route.substringAfter("profile/")
}

Advanced Navigation Patterns

Deep Linking Support

// commonMain/navigation/DeepLinkHandler.kt
class DeepLinkHandler {
fun handleDeepLink(url: String): String? {
return when {
url.contains("/profile/") -> {
val userId = url.substringAfter("/profile/")
Screen.Profile().createRoute(userId)
}
url.contains("/product/") -> {
val productId = url.substringAfter("/product/")
Screen.ProductDetail().createRoute(productId)
}
else -> null
}
}
}

Navigation with State Management

// commonMain/navigation/NavigationManager.kt
class NavigationManager(
private val navigationViewModel: NavigationViewModel
) {
fun navigateToUserProfile(userId: String) {
navigationViewModel.navigateTo(Screen.Profile().createRoute(userId))
}

fun navigateToProduct(productId: String) {
navigationViewModel.navigateTo(Screen.ProductDetail().createRoute(productId))
}

fun navigateToSettings() {
navigationViewModel.navigateTo(Screen.Settings.route)
}

fun navigateBack() {
navigationViewModel.navigateBack()
}

fun navigateHome() {
navigationViewModel.navigateTo(Screen.Home.route)
}
}

Best Practices

1. Code Organization

Structure your navigation code for maximum reusability:

commonMain/
├── navigation/
│ ├── NavigationDestinations.kt
│ ├── NavigationViewModel.kt
│ ├── NavigationManager.kt
│ └── DeepLinkHandler.kt
├── screens/
│ ├── home/
│ ├── profile/
│ └── settings/
└── ui/
└── components/
androidMain/
└── navigation/
└── AndroidNavigation.kt
iosMain/
└── navigation/
└── IOSNavigation.kt

2. Error Handling

// commonMain/navigation/NavigationErrorHandler.kt
sealed class NavigationError {
data class RouteNotFound(val route: String) : NavigationError()
data class InvalidParameters(val route: String, val error: String) : NavigationError()
data class PlatformNotSupported(val feature: String) : NavigationError()
}
class NavigationErrorHandler {
fun handleError(error: NavigationError) {
when (error) {
is NavigationError.RouteNotFound -> {
println("Route not found: ${error.route}")
// Navigate to fallback screen
}
is NavigationError.InvalidParameters -> {
println("Invalid parameters for route: ${error.route} - ${error.error}")
}
is NavigationError.PlatformNotSupported -> {
println("Feature not supported on this platform: ${error.feature}")
}
}
}
}

3. Testing Navigation

// commonTest/navigation/NavigationViewModelTest.kt
import kotlin.test.*
import kotlinx.coroutines.test.runTest
class NavigationViewModelTest {

@Test
fun `should navigate to correct destination`() = runTest {
val viewModel = NavigationViewModel()

viewModel.navigateTo("profile/123")

val state = viewModel.navigationState.value
assertEquals(NavigationState.Navigate("profile/123"), state)
}

@Test
fun `should clear navigation state`() = runTest {
val viewModel = NavigationViewModel()

viewModel.navigateTo("settings")
viewModel.clearNavigation()

val state = viewModel.navigationState.value
assertEquals(NavigationState.Idle, state)
}

@Test
fun `should handle back navigation`() = runTest {
val viewModel = NavigationViewModel()

viewModel.navigateBack()

val state = viewModel.navigationState.value
assertEquals(NavigationState.NavigateBack, state)
}
}

Performance Optimization

1. Lazy Screen Loading

@Composable
fun LazyScreen(content: @Composable () -> Unit) {
var isLoaded by remember { mutableStateOf(false) }

LaunchedEffect(Unit) {
isLoaded = true
}

if (isLoaded) {
content()
} else {
LoadingScreen()
}
}

2. Memory Management

// Use lifecycle-aware ViewModels
class ScreenViewModel : ViewModel() {
override fun onCleared() {
super.onCleared()
// Clean up resources
}
}

Migration from Legacy Navigation

If you’re migrating from older navigation patterns:

  1. Gradual Migration: Start with one screen and gradually convert others
  2. Maintain Compatibility: Keep old navigation working while testing new implementation
  3. Update Tests: Ensure all navigation tests are updated to work with the new system
  4. Platform Considerations: Test thoroughly on all target platforms

Future Considerations

As the Kotlin Multiplatform ecosystem evolves:

  • Navigation 3 Support: Monitor for official Navigation 3 multiplatform support
  • New Platform Targets: Prepare for additional platform support (Web, Desktop)
  • Performance Improvements: Stay updated with latest optimization techniques
  • Community Solutions: Engage with the community for shared navigation patterns

Conclusion

Navigation in Kotlin Multiplatform requires a thoughtful approach that balances code sharing with platform-specific requirements. By using the current stable navigation libraries and following the patterns outlined in this guide, you can build robust, maintainable navigation systems that work seamlessly across platforms.

Get Haider Ali Khan’s stories in your inbox

Join Medium for free to get updates from this writer.

The key principles to remember:

  1. Abstract Common Logic: Keep navigation state management in common code
  2. Platform-Specific Implementation: Allow platforms to handle navigation in their native way
  3. Type Safety: Use sealed classes and proper parameter handling
  4. Error Handling: Implement comprehensive error handling for navigation failures
  5. Testing: Write thorough tests for navigation logic
  6. Performance: Optimize for smooth navigation experiences

Remember that great navigation is invisible to users — it should feel natural and intuitive. By implementing these techniques thoughtfully, you’ll create applications that provide exceptional user experiences across all target platforms.

--

--

Haider Ali Khan

Written by Haider Ali Khan

Android & AI/ML Engineer. Native Android, Compose Multiplatform, scalable apps, clean code, and intelligent systems