# How to Implement Type-Safe Navigation Arguments in Decompose

> Learn how to implement type-safe navigation arguments in Decompose. Use generic StackNavigation and serializable config classes for compile-time safety and state restoration.

- Repository: [Arkadii Ivanov/decompose](https://github.com/arkivanov/decompose)
- Tags: how-to-guide
- Published: 2026-02-25

---

**Decompose achieves type-safe navigation by modeling the navigation stack as a generic `StackNavigation<Config>`, where a `@Serializable` configuration class carries all screen arguments, ensuring compile-time safety and automatic state restoration.**

Decompose, created by Arkadii Ivanov, is a Kotlin Multiplatform library for breaking down apps into lifecycle-aware components. Implementing type-safe navigation arguments in Decompose relies on a strongly-typed configuration stack that eliminates runtime casting errors and survives process death through built-in serialization.

## Understanding the Typed Navigation Stack

The foundation of type-safe navigation in Decompose is the `StackNavigation<C>` interface, defined in [`decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigation.kt`](https://github.com/arkivanov/decompose/blob/main/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigation.kt). This interface implements `StackNavigator<C>`, which provides the high-level navigation functions: `pushNew`, `pop`, `popTo`, and `navigate`.

By declaring your navigation instance with a specific configuration type—such as `StackNavigation<ScreenConfig>`—the compiler enforces that only instances of `ScreenConfig` can be pushed onto the stack. This generic constraint propagates through the entire navigation flow, from the navigation source to the child component factory.

## Defining Configuration Classes with Arguments

To pass arguments safely, you define a configuration class that holds all necessary data for a destination. Decompose requires this class to be `@Serializable` so the navigation stack can be saved and restored during configuration changes or process death.

### Data Classes for Simple Arguments

For straightforward scenarios, use a data class:

```kotlin
@Serializable
data class ItemConfig(val itemId: Long, val category: String)

```

### Sealed Classes for Multiple Screens

For applications with distinct screens, use a sealed class to enumerate all possible destinations:

```kotlin
@Serializable
sealed class ScreenConfig {
    @Serializable
    data object Home : ScreenConfig()
    
    @Serializable
    data class Details(val id: Int, val title: String) : ScreenConfig()
    
    @Serializable
    data class Edit(val itemId: String, val draft: String?) : ScreenConfig()
}

```

Each subclass carries its specific arguments, and the compiler ensures exhaustive `when` expressions in your child factory.

## Wiring Navigation with childStack

The `childStack` function, located in [`decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt`](https://github.com/arkivanov/decompose/blob/main/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt), connects your `StackNavigation` source to your component factory. It requires the configuration serializer to handle state restoration.

```kotlin
class RootComponent(
    componentContext: ComponentContext,
) : ComponentContext by componentContext {

    private val navigation = StackNavigation<ScreenConfig>()

    val stack: Value<ChildStack<*, MyScreen>> = childStack(
        source = navigation,
        serializer = ScreenConfig.serializer(),
        initialConfiguration = ScreenConfig.Home,
        childFactory = ::createScreen,
    )

    private fun createScreen(
        config: ScreenConfig,
        context: ComponentContext,
    ): MyScreen = when (config) {
        is ScreenConfig.Home -> HomeScreen(
            componentContext = context,
            onNavigateToDetails = { id, title ->
                navigation.pushNew(ScreenConfig.Details(id = id, title = title))
            }
        )
        is ScreenConfig.Details -> DetailsScreen(
            componentContext = context,
            id = config.id,
            title = config.title
        )
        is ScreenConfig.Edit -> EditScreen(
            componentContext = context,
            itemId = config.itemId,
            draft = config.draft
        )
    }
}

```

The `createScreen` function receives the exact `ScreenConfig` subtype, allowing direct access to arguments like `config.id` without casting.

## Navigating with Type-Safe Arguments

Decompose provides multiple ways to manipulate the navigation stack, all maintaining type safety through the generic `StackNavigator<C>` interface defined in [`decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigator.kt`](https://github.com/arkivanov/decompose/blob/main/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/StackNavigator.kt).

### Pushing New Screens with pushNew

The `pushNew` function adds a configuration to the top of the stack:

```kotlin
navigation.pushNew(ScreenConfig.Details(id = 42, title = "Type Safety"))

```

As seen in [`sample/shared/counters/DefaultCountersComponent.kt`](https://github.com/arkivanov/decompose/blob/main/sample/shared/counters/DefaultCountersComponent.kt), this pattern propagates arguments through the stack:

```kotlin
// From DefaultCountersComponent.kt
private val navigation = StackNavigation<Config>()

navigation.pushNew(Config(index = config.index + 1, isBackEnabled = true))

```

### Advanced Stack Manipulation with navigate

For complex transitions, use the `navigate` function, which provides the entire current stack as a `List<C>` and expects a new list in return. This enables reordering, filtering, or replacing multiple screens atomically.

The [`sample/shared/cards/DefaultCardsComponent.kt`](https://github.com/arkivanov/decompose/blob/main/sample/shared/cards/DefaultCardsComponent.kt) demonstrates bringing a specific card to the front:

```kotlin
navigation.navigate { stack ->
    val config = stack[index]          // Access typed Config safely
    listOf(config) + (stack - config) // Reorder: move to front
}

```

Because the lambda operates on `List<Config>`, accessing `config.id` or other properties requires no casting, and the compiler verifies that the returned list contains only `Config` instances.

## Summary

- **Type-safe navigation arguments in Decompose** rely on a generic `StackNavigation<Config>` where `Config` is a `@Serializable` class carrying all screen arguments.
- The **configuration class** acts as the single source of truth for navigation state, enabling automatic restoration after process death or configuration changes.
- **Compile-time safety** is enforced through Kotlin's type system: `pushNew`, `pop`, `popTo`, and `navigate` all operate exclusively on the declared `Config` type.
- **Child factories** receive the exact configuration instance, allowing direct access to arguments without runtime casting or bundle key management.

## Frequently Asked Questions

### Can I use type-safe navigation arguments with Decompose's slot navigation?

Yes. While this guide focuses on stack navigation, Decompose also provides `SlotNavigation<C>` for single-child (slot) navigation. The same principles apply: define a `@Serializable` configuration class, create `SlotNavigation<YourConfig>`, and use `childSlot` with the serializer. The compiler enforces type safety for arguments in slot navigation exactly as it does for stacks.

### How does Decompose handle argument serialization for state restoration?

Decompose uses `kotlinx.serialization` to persist the navigation stack. When you call `childStack` or `childSlot`, you provide a `serializer` parameter (e.g., `serializer = Config.serializer()`). During configuration changes or process death, Decompose serializes the list of configurations using this serializer and restores it upon recreation. This requires your configuration class to be marked with `@Serializable` and contain only serializable properties.

### What happens if I try to push a different type onto the stack?

The Kotlin compiler will raise a type error. Because `StackNavigation` is declared with a specific generic parameter (e.g., `StackNavigation<ScreenConfig>`), the `pushNew` method accepts only instances of that exact type. Attempting to pass a `String`, `Int`, or a different configuration class will fail at compile time, preventing runtime crashes from mismatched arguments.

### Can I pass complex objects or non-primitive types as navigation arguments?

Yes, provided they are serializable with `kotlinx.serialization`. Your configuration class can contain other `@Serializable` data classes, lists, maps, or enums. However, you cannot pass platform-specific types (like `Android View` or `Context`) or non-serializable objects (like lambdas or open classes) because Decompose must serialize the stack for state restoration. For complex dependencies, use dependency injection alongside the serializable configuration.