How to Implement Multi-Pane Navigation with ChildPanels in Decompose

The ChildPanels navigation model in Decompose enables responsive multi-pane layouts by composing up to three panels—Main, Details, and Extra—with automatic UI adaptation across SINGLE, DUAL, and TRIPLE modes.

The Decompose library by Arkadii Ivanov provides a robust navigation solution for Kotlin Multiplatform projects, with the ChildPanels API specifically designed for building responsive master-detail interfaces. This guide demonstrates how to implement multi-pane navigation with ChildPanels using the actual source implementation from the arkivanov/decompose repository, covering state management, navigation patterns, and Jetpack Compose integration.

Core Architecture of ChildPanels

The Child Panels model consists of three integrated layers that manage the lifecycle and presentation of up to three simultaneous panels.

The ChildPanels State Holder

At the center of the system is the ChildPanels data class defined in decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/ChildPanels.kt (lines 21–27). This immutable state holder contains the three possible child components—Main, Details, and Extra—along with the current ChildPanelsMode (SINGLE, DUAL, or TRIPLE). The class implements Serializable for automatic state preservation across process death.

The PanelsNavigator Interface

Navigation mutations occur through PanelsNavigator, which exposes imperative methods like activateDetails(), dismissDetails(), and setMode(). These extension functions are implemented in decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/PanelsNavigatorExt.kt (lines 85–94) and operate on the PanelsNavigation source to update the ChildPanels state atomically.

The ChildPanels Composable

The experimental Compose extension ChildPanels (located in extensions-compose-experimental/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/experimental/panels/ChildPanels.kt, lines 48–71) observes a Value<ChildPanels> and delegates layout decisions to a ChildPanelsLayout implementation. The default HorizontalChildPanelsLayout adapts its measuring policy based on the current mode, using configurable weights for DUAL and TRIPLE configurations.

Setting Up the Navigation Source

First, instantiate a PanelsNavigation source with appropriate configuration serializers, then initialize the navigation model using the childPanels extension on your ComponentContext.

// In your root component class
private val navigation = PanelsNavigation<Unit, DetailsConfig, Nothing>()

val panels: Value<ChildPanels<*, MainComponent, *, DetailsComponent, Nothing, Nothing>> =
    childPanels(
        source = navigation,
        serializers = Unit.serializer() to DetailsConfig.serializer(),
        initialPanels = { Panels(main = Unit) },
        handleBackButton = true,
        mainFactory = { _, ctx -> MainComponentImpl(ctx) },
        detailsFactory = { cfg, ctx ->
            DetailsComponentImpl(
                ctx,
                itemId = cfg.itemId,
                onFinished = navigation::dismissDetails
            )
        }
    )

This setup creates a navigation graph where the Main panel is always present, while Details and Extra panels can be activated or dismissed dynamically. The serializers parameter enables automatic state saving; pass null to both positions if you wish to disable state preservation.

Building the Multi-Pane UI

The UI layer consumes the Value<ChildPanels> and renders panels according to the current mode and available screen real estate.

Configuring the Layout Mode

The ChildPanelsMode enum determines how many panels display simultaneously:

  • SINGLE: Shows only the active panel (typical for phones)
  • DUAL: Splits space between Main and Details (tablets)
  • TRIPLE: Displays Main, Details, and Extra side-by-side (desktop)

Mode switching is a pure UI concern. Use PanelsNavigator.setMode() to update the state, triggering recomposition without affecting the back stack.

Implementing HorizontalChildPanelsLayout

The HorizontalChildPanelsLayout class (defined in extensions-compose-experimental/.../HorizontalChildPanelsLayout.kt) implements the measuring logic for responsive panel sizing. It accepts weight parameters for DUAL and TRIPLE configurations, converting them to concrete pixel widths via DualMeasurePolicy (lines 98–112) and TripleMeasurePolicy.

@Composable
fun MultiPaneContent(component: MultiPaneComponent) {
    val panels by component.panels.subscribeAsState()

    BoxWithConstraints(Modifier.fillMaxSize()) {
        // Main ChildPanels composable
        ChildPanels(
            panels = panels,
            mainChild = { MainContent(it.instance) },
            detailsChild = { DetailsContent(it.instance) },
            extraChild = { ExtraContent(it.instance) },
            layout = HorizontalChildPanelsLayout(
                dualWeights = Pair(0.3F, 0.7F),
                tripleWeights = Triple(0.3F, 0.4F, 0.3F)
            ),
            secondPanelPlaceholder = { Text("Select an article") },
            animators = ChildPanelsAnimators(
                single = fade() + scale(),
                dual = fade() to fade()
            ),
            predictiveBackParams = {
                PredictiveBackParams(
                    backHandler = component.backHandler,
                    onBack = component::onBack,
                    animatable = ::materialPredictiveBackAnimatable
                )
            }
        )

        // Responsive mode switching based on viewport width
        val mode = when {
            maxWidth >= 1200.dp -> ChildPanelsMode.TRIPLE
            maxWidth >= 800.dp -> ChildPanelsMode.DUAL
            else -> ChildPanelsMode.SINGLE
        }
        
        DisposableEffect(mode) { 
            component.setMode(mode) 
            onDispose {} 
        }
    }
}

The ChildPanels composable automatically handles placeholder visibility when panels are inactive and applies the specified ChildPanelsAnimators during mode transitions or navigation changes.

Use the navigator extension functions to manipulate the panel stack. These methods update the ChildPanels state, triggering UI recomposition.

// Activate Details from Main panel
onItemSelected = { itemId ->
    navigation.activateDetails(details = DetailsConfig(itemId = itemId))
}

// Activate Extra panel from Details (e.g., showing author info)
onShowAuthor = { authorId ->
    navigation.activateExtra(extra = AuthorConfig(authorId = authorId))
}

// Dismiss panels (typically called on back press)
navigation.dismissDetails()  // Returns to Main-only view
navigation.dismissExtra()    // Returns to Main+Details view
navigation.pop()             // Pops the top-most panel regardless of type

These navigation actions are defined in PanelsNavigatorExt.kt and maintain proper back stack ordering. When handleBackButton is enabled during initialization, the navigation automatically intercepts back presses to dismiss panels in reverse activation order.

Customizing Layout Weights

For granular control over panel proportions, customize the weights passed to HorizontalChildPanelsLayout. These values determine the fractional width allocated to each panel in DUAL and TRIPLE modes.

val customLayout = HorizontalChildPanelsLayout(
    dualWeights = Pair(0.25F, 0.75F),          // Main takes 25%, Details 75%
    tripleWeights = Triple(0.2F, 0.5F, 0.3F)  // Main/Details/Extra split
)

The layout engine applies these weights during the measure phase, ensuring panels resize smoothly when switching between modes or rotating devices.

Summary

  • ChildPanels manages up to three panels (Main, Details, Extra) with lifecycle awareness and state preservation via PanelsNavigation and the childPanels() factory.
  • ChildPanelsMode (SINGLE/DUAL/TRIPLE) controls the visual layout independently from the navigation state, enabling responsive designs that adapt to screen width.
  • HorizontalChildPanelsLayout implements the standard responsive behavior with configurable weight parameters for proportional sizing in multi-pane configurations.
  • Navigation mutations use imperative extension functions (activateDetails, dismissDetails, setMode) that update the observable ChildPanels state, driving automatic UI recomposition.
  • The experimental Compose extension provides ChildPanels composable with built-in support for placeholders, predictive back gestures, and custom animators.

Frequently Asked Questions

What is the maximum number of panels supported by ChildPanels?

ChildPanels supports exactly three panels: Main, Details, and Extra. The Main panel is typically the master view (e.g., a list), while Details and Extra represent increasingly specific content layers. The API is designed around this three-panel constraint, with HorizontalChildPanelsLayout providing optimized measuring policies for each combination of active panels.

How does ChildPanels handle state preservation during configuration changes?

State preservation relies on Kotlin serialization. When creating PanelsNavigation, you provide serializers for your configuration classes (MainConfig, DetailsConfig, etc.). The ChildPanels data class automatically serializes the current panel configurations and active mode during onSaveInstanceState, restoring them when the process recreates. Pass null to disable this behavior if your configurations contain non-serializable data.

Can I implement custom layouts beyond the horizontal arrangement?

Yes. While HorizontalChildPanelsLayout provides the standard left-to-right arrangement with weight-based sizing, you can implement the ChildPanelsLayout interface (defined in extensions-compose-experimental/.../ChildPanelsLayout.kt, lines 12–27) to create vertical stacks, overlapping panels, or context-aware arrangements. The ChildPanels composable accepts any ChildPanelsLayout implementation via its layout parameter.

How do I prevent the Details panel from appearing in SINGLE mode on phones?

The panel visibility logic is mode-driven, not device-driven. When ChildPanelsMode is set to SINGLE, only the active panel renders. To ensure phones never show split panes, detect the screen width (as shown in the BoxWithConstraints example) and set the mode to SINGLE when maxWidth < 800.dp. The placeholder content for inactive panels only appears in DUAL or TRIPLE modes when the corresponding panel slot is empty.

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 →