Skip to content

Navigation (KMP)

The KMP path uses the Decompose library for navigation. See the official Decompose navigation docs for the full API reference.

For advanced patterns, see Advanced navigation.

NavigationActions is a marker interface for defining navigation contracts:

interface HomeNavigation : NavigationActions {
    fun toDetail()
    fun toSettings()
}

NavigationActionsProducer<NAV> is an interface for components that expose a navigation contract:

class HomeComponent(
    componentContext: AppComponentContext,
    private val navigation: HomeNavigation,
) : BaseComponent<HomeState, HomeUiEvent>(componentContext, HomeState()),
    NavigationActionsProducer<HomeNavigation> {

    override val navigation = navigation

    fun onDetailClick() {
        navigation.toDetail()
    }
}

Parent component

Navigation is typically implemented in the parent component that manages the child stack. The parent creates a StackNavigation, defines configurations, and implements the NavigationActions interface for each child:

class RootComponent(
    componentContext: AppComponentContext,
) : BaseComponent<Unit, Nothing>(componentContext, Unit) {

    private val stackNavigation = StackNavigation<Config>()

    val childStack = childStack(
        source = stackNavigation,
        initialConfiguration = Config.Home,
        childFactory = ::createChild,
    )

    private fun createChild(config: Config, componentContext: ComponentContext) = when (config) {
        Config.Home -> HomeComponent(
            componentContext = componentContext,
            navigation = object : HomeNavigation {
                override fun toDetail() { stackNavigation.push(Config.Detail) }
                override fun toSettings() { stackNavigation.push(Config.Settings) }
            },
        )
        // ...
    }
}

switchTab

switchTab is an extension on StackNavigator<C> designed for bottom navigation. It works like bringToFront but does not recreate the configuration if its class is already on the stack. It brings the existing instance to the front and preserves its state.

// In a nav-host managing bottom navigation tabs
fun onTabSelected(tab: Tab) {
    val config = when (tab) {
        Tab.Home -> Config.Home
        Tab.Profile -> Config.Profile
    }
    stackNavigation.switchTab(config)
}

For apps with multiple sections (tabs, auth states), use a hierarchy of nav-host components. Each nav-host can own any combination of StackNavigation and SlotNavigation instances, for example a stack for the main flow and one or more slots for dialogs or bottom sheets. It implements the NavigationActions interfaces for the children it creates.

Screen components define a NavigationActions interface but never implement it. The parent nav-host provides the implementation as an anonymous object in createChild. This keeps screen components fully decoupled from the navigation structure above them.

A typical 3-level hierarchy looks like this:

RootNavHostComponent       - SlotNavigation: Login ↔ SignedIn
  SignedInNavHostComponent - StackNavigation: Home tab / Profile tab
    HomeNavHostComponent   - StackNavigation: First → Second → Third
// Top-level: switches between login and signed-in using a slot
class RootNavHostComponent(
    ctx: AppComponentContext,
) : BaseComponent<Unit, Nothing>(ctx, Unit) {

    private val navigation = SlotNavigation<RootConfig>()

    val child = childSlot(
        source = navigation,
        serializer = RootConfig.serializer(),
        initialConfiguration = { RootConfig.Login },
        childFactory = ::createChild,
    )

    private fun createChild(config: RootConfig, ctx: ComponentContext) = when (config) {
        RootConfig.Login -> LoginComponent(
            componentContext = ctx.toAppContext(),
            navigation = object : LoginNavigation {
                override fun onLoginSuccess() = navigation.activate(RootConfig.SignedIn)
            },
        )
        RootConfig.SignedIn -> SignedInNavHostComponent(ctx.toAppContext())
    }
}

// Second level: manages top-level tab stack
class SignedInNavHostComponent(
    ctx: AppComponentContext,
) : BaseComponent<Unit, Nothing>(ctx, Unit) {

    private val stackNavigation = StackNavigation<SignedInConfig>()

    val childStack = childStack(
        source = stackNavigation,
        serializer = SignedInConfig.serializer(),
        initialConfiguration = SignedInConfig.Home,
        childFactory = ::createChild,
    )

    private fun createChild(config: SignedInConfig, ctx: ComponentContext) = when (config) {
        SignedInConfig.Home -> HomeNavHostComponent(
            componentContext = ctx.toAppContext(),
            navigation = object : HomeNavHostNavigation {
                override fun toProfile() = stackNavigation.push(SignedInConfig.Profile)
            },
        )
        SignedInConfig.Profile -> ProfileComponent(
            componentContext = ctx.toAppContext(),
            navigation = object : ProfileNavigation {
                override fun back() = stackNavigation.pop()
            },
        )
    }
}

Navigation configs must be serializable

All navigation configurations must be @Serializable data classes or objects. Decompose uses serialization to preserve the full navigation stack across process death and configuration changes.