How ChildStack Manages Back Stack Navigation State in Decompose

ChildStack maintains back stack navigation state as an immutable list of configurations where the active child represents the top of the stack and preceding elements form the history, transformed through StackNavState and mapped to component instances via the childStack factory.

In the Decompose navigation library by Arkadii Ivanov, ChildStack serves as the core data structure for representing hierarchical navigation history. Understanding how this class manages the relationship between active and inactive children is essential for building predictable, testable navigation flows in Kotlin Multiplatform applications.

The ChildStack Data Structure

The foundation of back stack management resides in [ChildStack.kt], which defines the ChildStack class as a simple but strict state holder.

ChildStack stores two critical properties:

  • active: Child.Created<C, T> – Represents the currently visible (top) child component and its configuration
  • backStack: List<Child.Created<C, T>> – Represents the history of inactive children maintained in order

The items property exposes the complete stack as a unified list via a GettingList implementation, combining backStack + active so UI layers can iterate over the full navigation history without managing indices manually.

// Conceptual structure from ChildStack.kt
data class ChildStack<out C : Any, out T : Any>(
    val active: Child.Created<C, T>,
    val backStack: List<Child.Created<C, T>> = emptyList()
) {
    val items: List<Child.Created<C, T>> = GettingList(backStack.size + 1) { index ->
        if (index == backStack.size) active else backStack[index]
    }
}

Creating and Updating the Navigation Stack

Navigation transformations occur in [ChildStackFactory.kt] through the childStack factory functions. These functions initialize a children navigation source that manages the lifecycle of components based on configuration changes.

The factory accepts several key parameters:

  1. initialStack – Supplies the starting list of configurations (ordered from bottom to top)
  2. navTransformer – Defines how to transform the current StackNavState when navigation events occur (push, pop, replace)
  3. serializer – Optional KSerializer for state persistence

Internally, the factory maintains a StackNavState class (defined within ChildStackFactory.kt) that holds the immutable configurations: List<C> list. Every navigation event creates a new StackNavState instance with a modified configuration list, ensuring thread-safe, predictable state updates.

// From ChildStackFactory.kt - conceptual initialization
val stack: Value<ChildStack<Config, MyComponent>> = childStack(
    source = navigation,
    initialStack = { listOf(HomeConfig) },
    handleBackButton = true,
    childFactory = { config, componentContext -> 
        createComponent(config, componentContext) 
    }
)

Mapping Configurations to ChildStack Instances

The bridge between raw configuration lists and typed component instances happens through the stateMapper parameter inside the childStack factory.

When configurations change, the navigation source creates child components for each configuration in the stack. The stateMapper receives this List<Child.Created<C, T>> and constructs the final ChildStack:

// Implementation detail from ChildStackFactory.kt
ChildStack(
    active = createdChildren.last(),
    backStack = createdChildren.dropLast(1),
)

This mapping ensures that:

  • The active child is always the last element in the created children list (top of stack)
  • The back stack contains everything preceding the active child in historical order
  • The structure remains immutable; every navigation operation produces a new ChildStack instance

Back Button Handling

Decompose provides declarative back button management through the backTransformer parameter when handleBackButton = true.

The transformer inspects the current StackNavState size to determine if a pop operation is valid:

  • If size > 1: Returns a lambda executing dropLast(1) on the configurations list, removing the active configuration and exposing the previous one
  • If size == 1: Returns null, preventing the back button from popping the root screen
// Back handling logic from ChildStackFactory.kt
backTransformer = { state ->
    if (state.configurations.size > 1) {
        { state.copy(configurations = state.configurations.dropLast(1)) }
    } else {
        null
    }
}

This approach keeps navigation logic pure and testable, with the actual back button dispatch handled by the Decompose framework.

State Persistence Across Process Death

The back stack navigation state survives process death through the optional serialization layer. When provided with a serializer, the childStack factory:

  1. Saves state: Serializes the List<C> configurations into a SerializableContainer using the provided KSerializer
  2. Restores state: Deserializes the configuration list on recreation, passing it to restoreStack callback before creating components

This ensures users return to the exact same navigation depth and history after background termination, with the entire configuration stack (not just the active screen) recovered accurately.

Compose UI Integration

The Compose-specific extension in [ChildStack.kt] (Compose module) renders the stack while managing UI state retention.

The composable accepts a Value<ChildStack> and uses SaveableStateHolder to preserve the UI state (scroll positions, text input, etc.) for every child in the stack, not just the active one. When a child leaves the back stack entirely, its associated saved state is automatically purged to prevent memory leaks.

// Compose usage from ChildStack.kt (Compose extension)
@Composable
fun MyApp() {
    ChildStack(
        stack = stack,
        animation = stackAnimation(slide()),
    ) { child ->
        // child.instance is the active component
        when (child.configuration) {
            is HomeConfig -> HomeScreen(child.instance)
            is DetailsConfig -> DetailsScreen(child.instance)
        }
    }
}

Summary

  • ChildStack in [ChildStack.kt] stores navigation state as active (top) plus backStack (history) using immutable data structures
  • childStack factory in [ChildStackFactory.kt] transforms configuration lists through StackNavState and maps them to component instances
  • Back navigation uses dropLast(1) on the configuration list when handleBackButton is enabled and the stack depth exceeds one
  • State persistence serializes the configuration list via the provided serializer, restoring the complete back stack across process death
  • Compose integration retains UI state for all children in the stack using SaveableStateHolder, cleaning up only when children are permanently removed

Frequently Asked Questions

How is the back stack represented internally in ChildStack?

The back stack is represented as a List<Child.Created<C, T>> stored in the backStack property of the ChildStack class defined in [ChildStack.kt]. This list contains all inactive children in historical order, while the active property holds the top child separately. The items property merges both into a single iterable view for convenience.

What happens when the back button is pressed in a ChildStack?

When handleBackButton = true, the backTransformer in [ChildStackFactory.kt] checks if state.configurations.size > 1. If true, it returns a transformation that executes dropLast(1) on the configuration list, effectively removing the active configuration and restoring the previous one as active. If only one configuration remains, it returns null to prevent exiting the root screen.

Can the back stack state survive process death?

Yes. By providing a serializer parameter to the childStack factory, the list of configurations is serialized into a SerializableContainer when the app enters the background. Upon restoration, the factory uses restoreStack to recreate the exact configuration list, reconstructing the entire navigation history including the back stack depth and specific configurations.

How does Compose retain UI state for back stack entries?

The Compose extension [ChildStack.kt] uses SaveableStateHolder to retain UI state (such as scroll position or form input) for every child present in the stack. When a child moves from active to back stack, its state is preserved. When a configuration is permanently removed from the stack (popped), the associated saved state is automatically cleared to maintain memory efficiency.

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 →