Composables

KMP Navigation

Community

ahmed3elshaer

Navigation libraries for KMP. Voyager, Decompose, and platform-specific navigation (Compose Navigation, SwiftUI).

Overview

Repository
ahmed3elshaer/everything-claude-code-mobile
Publisher
ahmed3elshaer
Stars
66
Forks
18
Bundled files
0
License
MIT
Updated
Jun 14, 2026

Installation

tmp=$(mktemp -d)
git clone --depth 1 https://github.com/ahmed3elshaer/everything-claude-code-mobile.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/skills/kmp-navigation" ~/.claude/skills/kmp-navigation
rm -rf "$tmp"

Instructions

KMP Navigation

Cross-platform navigation strategies for Kotlin Multiplatform.

Navigation Options

1. Voyager (Recommended)
// build.gradle.kts
dependencies {
    implementation("cafe.adriel.voyager:voyager-navigator:1.0.0")
    implementation("cafe.adriel.voyager:voyager-screen-model:1.0.0")
}
// commonMain/kotlin/navigation/VoyagerNavigation.kt
@Serializable
data object HomeScreen : Screen

@Serializable
data class DetailScreen(val id: String) : Screen

@Composable
fun AppNavigation() {
    Navigator(HomeScreen) { navigator ->
        VoyagerNavigation(
            navigator = navigator,
            screenModels = { rememberScreenModel { AppScreenModel() } }
        )
    }
}

@Composable
fun HomeScreen() {
    Screen { // Voyager's Screen composable
        val navigator = LocalNavigator.currentOrThrow

        Button(onClick = { navigator.push(DetailScreen("123")) }) {
            Text("Go to Detail")
        }
    }
}
2. Decompose
// build.gradle.kts
dependencies {
    implementation("com.arkivanov.decompose:decompose:2.1.0")
    implementation("com.arkivanov.decompose:extensions-compose-jetpack:2.1.0")
}
// commonMain/kotlin/navigation/DecomposeNavigation.kt
sealed class Config : Parcelable {
    @Parcelize
    data object Home : Config()

    @Parcelize
    data class Detail(val id: String) : Config()
}

@Composable
fun AppNavigation() {
    val navigator = rememberNavigator()

    Decomanavigation(
        navigator = navigator,
        initialConfiguration = Config.Home
    ) {
        when (val config = it.configuration) {
            is Config.Home -> HomeScreen(
                onNavigateToDetail = { navigator.push(Config.Detail(it)) }
            )
            is Config.Detail -> DetailScreen(
                id = config.id,
                onBack = { navigator.pop() }
            )
        }
    }
}
3. Platform-Native Bridge
// commonMain/kotlin/navigation/Navigator.kt
sealed class Screen : Parcelable {
    @Parcelize
    data object Home : Screen()

    @Parcelize
    data class Detail(val id: String) : Screen()
}

interface Navigator {
    val navigationStack: StateFlow<List<Screen>>
    fun navigateTo(screen: Screen)
    fun navigateBack()
}

expect class Navigator() : Navigator

// androidMain - Compose Navigation
actual class Navigator : Navigator {
    private val _stack = MutableStateFlow(listOf(Screen.Home))
    override val navigationStack: StateFlow<List<Screen>> = _stack.asStateFlow()

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

        NavHost(navController, startDestination = "home") {
            composable("home") {
                HomeScreen(
                    onNavigateToDetail = { navController.navigate("detail/$it") }
                )
            }
            composable("detail/{id}") { backStackEntry ->
                val id = backStackEntry.arguments?.getString("id") ?: ""
                DetailScreen(id, onBack = { navController.popBackStack() })
            }
        }
    }
}

// iosMain - SwiftUI Navigation wrapper
actual class Navigator : Navigator {
    private val _stack = MutableStateFlow(listOf(Screen.Home))
    override val navigationStack: StateFlow<List<Screen>> = _stack.asStateFlow()

    fun toSwiftUI() -> some View {
        // Bridge to SwiftUI NavigationStack
    }
}

Tab Navigation

Voyager Tabs
// commonMain/kotlin/navigation/TabNavigation.kt
enum class Tab {
    HOME,
    SEARCH,
    PROFILE
}

@Composable
fun TabNavigation() {
    val navigator = rememberTabNavigator()

    TabNavigator(tab = navigator.current) {
        // Tab content
    }
}
Decompose Tabs
// commonMain/kotlin/navigation/Tabs.kt
enum class Tab {
    HOME,
    SEARCH,
    PROFILE
}

@Composable
fun Tabs(
    navigator: StackNavigator,
    selectedTab: MutableState<Tab>
) {
    Row {
        Tab.values().forEach { tab ->
            Button(
                onClick = { selectedTab.value = tab }
            ) {
                Text(tab.name)
            }
        }
    }
}

Deep Linking

// commonMain/kotlin/navigation/DeepLink.kt
sealed class Screen : Parcelable {
    @Parcelize
    data object Home : Screen()

    @Parcelize
    data class Detail(val id: String) : Screen()

    companion object {
        fun fromDeepLink(url: String): Screen? {
            return when {
                url.contains("/home") -> Home
                "/detail/([a-z0-9]+)".toRegex().find(url) != null -> {
                    val id = "/detail/([a-z0-9]+)".toRegex().find(url)!!.groupValues[1]
                    Detail(id)
                }
                else -> null
            }
        }
    }
}

Navigation Arguments

Type-Safe Arguments
// ✅ Sealed class with arguments
@Serializable
sealed class Screen : Parcelable {
    @Parcelize
    data object Home : Screen()

    @Parcelize
    data class UserDetail(
        val userId: String,
        val section: String? = null
    ) : Screen()

    @Parcelize
    data class EditItem(
        val itemId: String,
        val mode: EditMode = EditMode.View
    ) : Screen()
}

@Serializable
enum class EditMode {
    View, Edit, Create
}

State Preservation

Voyager ScreenModel
class DetailScreenModel(
    private val userId: String,
    private val repository: UserRepository
) : ScreenModel {
    val user = mutableStateOf<User?>(null)
    val isLoading = mutableStateOf(false)

    init {
        loadUser()
    }

    private fun loadUser() {
        viewModelScope.launch {
            isLoading.value = true
            user.value = repository.getUser(userId)
            isLoading.value = false
        }
    }
}

@Composable
fun DetailScreen(
    userId: String,
    onBack: () -> Unit
) {
    val model = rememberScreenModel { DetailScreenModel(userId) }

    if (model.isLoading.value) {
        CircularProgressIndicator()
    } else {
        model.user.value?.let { user ->
            UserContent(user, onBack)
        }
    }
}

Remember: Choose navigation library based on project needs. Voyager for simplicity, Decompose for control, platform-native for full platform feature support.

FAQ

What does KMP Navigation do?

Navigation libraries for KMP. Voyager, Decompose, and platform-specific navigation (Compose Navigation, SwiftUI).

Who made KMP Navigation?

ahmed3elshaer (a community member) publishes it in the ahmed3elshaer/everything-claude-code-mobile repository on GitHub.

Is KMP Navigation free?

Yes. KMP Navigation is open source, published by ahmed3elshaer under the MIT license.

What comes with KMP Navigation?

Just its instructions. Everything the agent needs is in one SKILL.md file.

Which agents can use KMP Navigation?

Any coding agent that reads SKILL.md files, including Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI. The agent loads the skill when a task matches its description.

What are agent skills?

An agent skill is a folder with a SKILL.md file: instructions that teach a coding agent how to do one kind of task, and optionally files it can read while it works. The agent keeps only the skill's name and description in mind and reads the rest when a task calls for it.

How are agent skills different from MCP servers?

An MCP server gives an agent new tools to call, code that runs somewhere and returns results. A skill gives it knowledge: how to approach a task with the tools it already has. Many agents use both.

How can my agent test my Android app?

Robomori lets coding agents create, start and control Android emulators and devices from one command, so they can install your app, take screenshots and check dark mode, font sizes and languages.

You're subscribed

The next lesson lands in on Wednesday.