How to Implement Deep Linking with StackNavigation in Decompose

Decompose supports deep linking by parsing a URL into path segments and query parameters, then passing the resulting Url object through the component hierarchy to build the initial navigation stack.

Decompose is a Kotlin Multiplatform library for building component-based architectures with navigation. Implementing deep linking with StackNavigation in Decompose requires parsing incoming URLs and mapping them to your navigation configuration tree. The library provides childStackWebNavigation and URL parsing utilities to bridge browser history with your component stack.

Parse Incoming URLs into Path Segments

Before components can handle deep links, you must parse the raw URL into a structured format. In src/commonMain/kotlin/com/arkivanov/sample/shared/Url.kt, the Url data class separates path segments from query parameters:

data class Url(
    val pathSegments: List<String>, 
    val parameters: Map<String, String>
)

This lightweight representation allows components to consume path segments sequentially using consumePathSegment(), which returns the first segment and a new Url with the remaining segments for child components.

Inject Deep Links at the Entry Point

For web targets, deep links arrive through the browser's address bar. In src/app-js/src/main/kotlin/com/arkivanov/sample/app/Main.kt, the withWebHistory function captures the initial URL and passes it to your root component:

@OptIn(ExperimentalDecomposeApi::class)
fun main() {
    val lifecycle = LifecycleRegistry()

    val root = withWebHistory { _, deepLink ->
        DefaultRootComponent(
            componentContext = DefaultComponentContext(
                lifecycle = lifecycle, 
                stateKeeper = stateKeeper
            ),
            featureInstaller = DefaultFeatureInstaller,
            deepLinkUrl = deepLink?.let(::Url)  // Parse raw string to Url
        )
    }
    // ...
}

The deepLink parameter contains the raw URL string from the browser, which you convert to your Url type before injection.

Build the Initial Navigation Stack

The root component interprets the Url to determine which screens should be active. In src/commonMain/kotlin/com/arkivanov/sample/shared/root/DefaultRootComponent.kt, the getInitialStack method maps path segments to configuration objects:

private fun getInitialStack(deepLinkUrl: Url?): List<Config> {
    val (path, childUrl) = deepLinkUrl?.consumePathSegment() 
        ?: return listOf(Config.Tabs())
    
    return when (path) {
        pathSegmentOf<Config.DynamicFeatures>() -> 
            listOf(Config.Tabs(), Config.DynamicFeatures)
        pathSegmentOf<Config.CustomNavigation>() -> 
            listOf(Config.Tabs(), Config.CustomNavigation)
        pathSegmentOf<Config.Pages>() -> 
            listOf(Config.Tabs(), Config.Pages(deepLinkUrl = childUrl))
        pathSegmentOf<Config.SharedTransitions>() -> 
            listOf(Config.Tabs(), Config.SharedTransitions(deepLinkUrl = childUrl))
        else -> listOf(Config.Tabs(deepLinkUrl = childUrl))
    }
}

This approach supports nested deep linking by passing the remaining childUrl to child components that accept their own deepLinkUrl parameters.

Handle Nested Deep Linking in Child Components

Child stacks can implement the same pattern to handle their own path segments. In src/commonMain/kotlin/com/arkivanov/sample/shared/tabs/DefaultTabsComponent.kt, the component extracts its segment and passes the remainder to its own children:

private fun getInitialConfig(deepLinkUrl: Url?): Config {
    val (path, childUrl) = deepLinkUrl?.consumePathSegment() 
        ?: return Config.Menu
    
    return when (path) {
        pathSegmentOf<Config.Counters>() -> Config.Counters
        pathSegmentOf<Config.Cards>() -> Config.Cards
        pathSegmentOf<Config.MultiPane>() -> Config.MultiPane(deepLinkUrl = childUrl)
        else -> Config.Menu
    }
}

This recursive pattern allows URLs like /tabs/multi_pane/123 to navigate through multiple levels of the component hierarchy.

Synchronize with Browser History

To enable two-way synchronization between your navigation stack and the browser's address bar, expose a WebNavigation implementation using childStackWebNavigation. In DefaultRootComponent.kt:

override val webNavigation: WebNavigation<*> = childStackWebNavigation(
    navigator = nav,
    stack = _stack,
    serializer = Config.serializer(),
    pathMapper = { it.configuration.path() },  // Config -> "/pages/123"
    childSelector = {
        when (val child = it.instance) {
            is TabsChild -> child.component
            is PagesChild -> child.component
            is SharedTransitionsChild -> child.component
            else -> null
        }
    }
)

The pathMapper converts configurations to URL paths, while childSelector identifies which child component should handle web navigation for a given stack entry.

Summary

  • Parse URLs using a lightweight Url data class that separates path segments from query parameters.
  • Inject deep links at the platform entry point (e.g., withWebHistory for web) and pass them to your root component.
  • Build initial stacks by consuming path segments sequentially with consumePathSegment(), mapping each segment to navigation configurations.
  • Support nesting by passing remaining URL segments to child components that implement their own deep link handling.
  • Enable browser sync by exposing WebNavigation via childStackWebNavigation with appropriate pathMapper and childSelector functions.

Frequently Asked Questions

The Url data class stores query parameters in a Map<String, String>. After parsing the raw URL, components can access these parameters via url.parameters to configure initial state, such as pre-filling search fields or setting filter options.

Can deep linking work on mobile platforms (iOS/Android) as well?

Yes. While the examples show withWebHistory for web, mobile platforms receive deep links through platform-specific APIs (e.g., Android Intents or iOS Universal Links). You parse the incoming URL string into your Url type and pass it to the root component exactly as shown in the web example.

If consumePathSegment() returns a path that doesn't match known configurations, you should fall back to a default configuration (typically the root or home screen). In the sample code, the else branch returns Config.Tabs() or Config.Menu to ensure the app always launches into a valid state.

Yes. The pattern supports arbitrary nesting depth. Each component consumes its own path segment and passes the remaining childUrl to the next level. For example, a URL /tabs/multi_pane/123 would activate the Tabs stack, select the MultiPane tab, and pass 123 to the MultiPane component's own navigation stack.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →