How to Implement Custom Navigation Using ChildrenFactory in Decompose

The children factory extension provides the low-level API for building custom navigation models in Decompose, orchestrating state persistence, lifecycle transitions, and back-button handling through immutable NavState snapshots and pure-function navigation transformations.

For navigation patterns that extend beyond standard stacks or slots, the arkivanov/decompose library offers the ChildrenFactory API. This extension allows you to implement custom navigation using ChildrenFactory in Decompose by defining immutable navigation states and controlling every aspect of child component lifecycles. Unlike high-level helpers such as childStack or childSlot, the Children Factory gives you direct control over which children are created, started, resumed, or destroyed at any given moment.

Core Components of the Children Factory API

Three interfaces form the foundation of custom navigation in Decompose. Understanding their roles is essential before implementing your own navigation model.

NavState<C> is an immutable snapshot of your entire navigation hierarchy. Defined in decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/NavState.kt, it exposes a single property: children: List<ChildNavState<C>>. This list must contain one entry for every possible child configuration, with each element annotated with a lifecycle status.

ChildNavState<C> represents a single child’s configuration alongside its lifecycle state. Located in decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildNavState.kt, this interface tracks the configuration: C and a status that can be CREATED, STARTED, RESUMED, or DESTROYED. Only one child typically holds the RESUMED status at a time, indicating it is currently visible and interactive.

ChildrenFactory (the children(...) extension function) orchestrates the entire navigation system. Found in decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenFactory.kt, it receives a NavigationSource<E> (commonly SimpleNavigation), manages state serialization, handles back-button callbacks, and instantiates child components through a factory lambda.

Step-by-Step Implementation Guide

Step 1: Define Your Navigation State

Create a serializable data class that implements NavState<C>. This class represents the complete state of your navigation graph at any moment. In DefaultCustomNavigationComponent.kt, the sample implements a carousel/pager navigation model:

@Serializable
private data class Config(val imageResourceId: ImageResourceId)

@Serializable
private data class NavigationState(
    val configurations: List<Config>,
    val index: Int,
    val mode: Mode,
) : NavState<Config> {

    override val children: List<SimpleChildNavState<Config>> by lazy {
        configurations.mapIndexed { i, cfg ->
            SimpleChildNavState(
                configuration = cfg,
                status = if (i == index) ChildNavState.Status.RESUMED
                         else ChildNavState.Status.CREATED
            )
        }
    }
}

The children property must return a list where every possible configuration is represented exactly once. The status determines which component receives lifecycle callbacks—only the RESUMED child is actively visible, while others remain CREATED (preserved in memory but stopped).

Step 2: Create a Navigation Source

Instantiate a SimpleNavigation source that emits state transformation functions. This pattern, defined in decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/SimpleNavigation.kt, maintains a queue of pending navigation actions expressed as pure functions:

private val navigation = SimpleNavigation<(NavigationState) -> NavigationState>()

SimpleNavigation acts as a NavigationSource<(S) -> S>, allowing you to push immutable state updates without exposing mutable state to your UI layer.

Step 3: Configure the Children Factory

Invoke the children() extension from within your ComponentContext to wire everything together. This function requires several key parameters to implement custom navigation using ChildrenFactory in Decompose:

private val _children: Value<Children<Config, KittenComponent>> = children(
    source = navigation,
    stateSerializer = NavigationState.serializer(),
    key = "carousel",
    initialState = {
        NavigationState(
            configurations = ImageResourceId.entries.map(::Config),
            index = 0,
            mode = Mode.CAROUSEL
        )
    },
    navTransformer = { state, transform -> transform(state) },
    stateMapper = { state, children ->
        Children(
            items = children.map { it as Child.Created },
            index = state.index,
            mode = state.mode
        )
    },
    backTransformer = {
        it.takeIf { it.index > 0 }?.let { state ->
            { state.copy(index = state.index - 1) }
        }
    },
    childFactory = { config, ctx ->
        DefaultKittenComponent(componentContext = ctx, imageResourceId = config.imageResourceId)
    }
)

Key parameters explained:

  • navTransformer: Receives the current NavState and a navigation event (the transformer function from your source), returning the next state.
  • stateMapper: Converts the raw NavState and list of created children into a UI-specific model wrapped in Value<S>.
  • backTransformer: Optionally returns a function that produces a new state when the back button is pressed. Returning null disables back handling for the current state.
  • childFactory: Creates actual component instances for each configuration, receiving the configuration and a child-specific ComponentContext.

Step 4: Trigger Navigation Events

Navigation actions are expressed as pure functions that receive the current state and return a transformed copy. This ensures immutability and makes testing straightforward:

fun onForwardClicked() {
    navigation.navigate { state ->
        state.copy(index = (state.index + 1) % state.configurations.size)
    }
}

All calls to navigation.navigate are queued and applied sequentially, ensuring thread safety when called from the main thread (strongly recommended by the API documentation).

Internal Architecture and Lifecycle Management

When you invoke children(), Decompose instantiates a ChildrenNavigator internally (located in decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ChildrenNavigator.kt). This class performs several critical operations:

  • Diff calculation: Compares the previous list of ChildNavState with the new one to determine which children need creation, destruction, or lifecycle transitions.
  • Lifecycle orchestration: Automatically moves children between CREATED, STARTED, and RESUMED states based on your NavState updates.
  • State persistence: Uses the provided stateSerializer with the component's StateKeeper to automatically save and restore navigation state across configuration changes.
  • Back handling: Registers a BackCallback that is enabled only when backTransformer returns a non-null function for the current state, ensuring the back button respects your custom navigation logic.

Summary

  • ChildrenFactory is the low-level API in arkivanov/decompose for implementing navigation patterns beyond stacks and slots.
  • NavState and ChildNavState define immutable snapshots of your navigation hierarchy and individual child statuses.
  • SimpleNavigation provides a thread-safe event source using pure function transformations.
  • The children() function requires navTransformer, stateMapper, backTransformer, and childFactory to bridge state changes with component lifecycles.
  • Source files in decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/children/ contain the core interfaces, while DefaultCustomNavigationComponent.kt provides a working carousel example.

Frequently Asked Questions

What is the difference between ChildrenFactory and childStack?

childStack is a high-level convenience API built on top of ChildrenFactory specifically for back-stack navigation with push/pop semantics. ChildrenFactory exposes the underlying primitive that powers childStack, allowing you to define arbitrary navigation shapes such as carousels, wizard steps, or multi-pane layouts where multiple children might be visible simultaneously.

How does state preservation work with custom navigation?

Decompose automatically persists your navigation state using the stateSerializer parameter passed to the children() function. When the system kills and recreates your process, the NavState is deserialized and passed as the initialState, restoring your exact navigation position including the lifecycle status of every child component.

Can I use ChildrenFactory for multi-pane or master-detail layouts?

Yes. Unlike childStack, which assumes a single active child, ChildrenFactory supports multiple RESUMED children simultaneously. By setting several children to ChildNavState.Status.RESUMED in your NavState implementation, you can keep multiple panes active—such as a master list and detail view side-by-side on tablets.

How do I disable the back button for specific navigation states?

Return null from the backTransformer lambda for states where back navigation should be disabled. The ChildrenNavigator only registers an active BackCallback when backTransformer produces a non-null transformation function, effectively disabling system back gestures when your UI is in a state that should not be popped (such as the first step of a wizard).

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 →