How ChildPages Handles Pager-Like Navigation in Decompose: Architecture and Implementation

ChildPages is a composable wrapper that bridges Decompose’s pages navigation model with Jetpack Compose’s pager APIs, synchronizing swipe gestures with the navigation state while managing component lifecycles lazily.

The ChildPages API in the arkivanov/decompose library provides a declarative way to implement swipeable pager interfaces in Kotlin Multiplatform applications. By connecting the ChildPages<C, T> navigation model to Compose's HorizontalPager or VerticalPager, developers can create native-feeling pager navigation while maintaining Decompose's rigorous lifecycle management and state preservation across configuration changes.

What Is ChildPages in Decompose?

Decompose separates navigation logic from UI implementation. The navigation layer defines ChildPages<C, T>—an immutable data class representing a list of child components with a selected index. The UI layer consumes this state through the ChildPages composable, which adapts it to Compose's pager widgets.

This architecture ensures that business logic remains testable and platform-agnostic, while the Compose extension handles platform-specific interactions like swipe gestures and animations.

Core Architecture of Pager Navigation

The implementation in extensions-compose/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/pages/ChildPages.kt follows a six-step pipeline to synchronize navigation state with UI state.

Observing the Navigation Model

The composable begins by observing the navigation model through pages.subscribeAsState(), which converts Decompose's Value<ChildPages<C, T>> into a Compose State. This provides reactive updates whenever the navigation model changes, such as when PagesNavigator.select(index) is called programmatically.

Deriving the Pager State

From the observed ChildPages instance, the composable constructs a PagerState with two critical parameters:

  • currentPage is set to selectedIndex (clamped to ≥ 0 to prevent illegal indexes when the list is empty)
  • pageCount dynamically reads pages.items.size

This ensures the pager widget always reflects the current navigation state, even as children are added or removed.

Handling Scroll Animations

Inside LaunchedEffect(selectedIndex), Decompose checks state.isScrollInProgress to avoid interrupting active user gestures. When safe to proceed, it executes one of three strategies defined by the PagesScrollAnimation sealed interface:

  • Disabled → Calls state.scrollToPage(selectedIndex) for instant jumps
  • Default → Calls state.animateScrollToPage(selectedIndex) using Compose's default animation curve
  • Custom → Calls state.animateScrollToPage(selectedIndex, animationSpec = scrollAnimation.spec) with a user-provided AnimationSpec<Float>

The PagesScrollAnimation contract is defined in extensions-compose/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/pages/PagesScrollAnimation.kt.

Synchronizing User Interactions

To propagate swipe gestures back to the navigation model, the composable uses DisposableEffect(state.currentPage, state.targetPage). When the pager settles (state.currentPage == state.targetPage), it invokes the user-supplied onPageSelected(state.currentPage) callback.

This callback should execute PagesNavigator.select(index) to update the navigation model, creating a bidirectional sync between UI gestures and application state.

Rendering Child Components

The pager composable parameter (defaulting to defaultHorizontalPager()) receives:

  • A modifier for layout control
  • The derived state
  • A stable key derived from pages.items[it] to prevent unnecessary recompositions
  • A pageContent lambda

Inside pageContent, the child instance is cached using Ref to survive recompositions. If item.instance is non-null, it is stored in pageRef and passed to the content lambda, ensuring lazy initialization and proper lifecycle management.

Implementing ChildPages in Your Compose UI

Below is a minimal implementation demonstrating horizontal pager integration with default scroll animations.

@Composable
fun SamplePages(
    pages: Value<ChildPages<PageConfig, PageComponent>>,
    navigator: PagesNavigator<PageConfig>
) {
    ChildPages(
        pages = pages,
        onPageSelected = { index -> navigator.select(index) },
        scrollAnimation = PagesScrollAnimation.Default,
        pager = defaultHorizontalPager(),
        key = { child -> child.key },
        pageContent = { _, page, _ ->
            page.Render()
        }
    )
}

Customizing Pager Behavior

Vertical Pager Configuration

To implement a vertical scrolling pager, substitute the pager implementation:

ChildPages(
    pages = pages,
    onPageSelected = { navigator.select(it) },
    pager = defaultVerticalPager(),
    pageContent = { _, page, _ -> page.Render() }
)

Custom Scroll Animations

For non-standard transitions, provide a custom animation specification:

val springSpec = spring<Float>(stiffness = Spring.StiffnessLow)

ChildPages(
    pages = pages,
    onPageSelected = { navigator.select(it) },
    scrollAnimation = PagesScrollAnimation.Custom(springSpec),
    pageContent = { _, page, _ -> page.Render() }
)

Key Source Files and Responsibilities

File Responsibility
extensions-compose/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/pages/ChildPages.kt Main composable implementing the bridge between ChildPages navigation state and Compose pager widgets.
extensions-compose/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/pages/PagesScrollAnimation.kt Sealed interface defining animation strategies (Disabled, Default, Custom).
decompose/router/pages/ChildPages.kt Defines the immutable ChildPages<C, T> data class representing the navigation model.

Summary

  • ChildPages synchronizes Decompose's pages navigation model with Jetpack Compose's pager widgets through a declarative composable interface.
  • Bidirectional sync ensures that programmatic navigation updates the UI via LaunchedEffect, while user swipes update the model via onPageSelected callbacks.
  • Lifecycle management creates child components lazily and caches them through recompositions using Ref, ensuring optimal performance.
  • Customization supports horizontal or vertical orientations through defaultHorizontalPager() and defaultVerticalPager(), plus configurable animations via PagesScrollAnimation.

Frequently Asked Questions

What is the difference between ChildPages and ChildStack in Decompose?

ChildStack manages a stack-based navigation history where new components are pushed onto a stack and back navigation pops them off, ideal for drill-down flows. ChildPages maintains a flat list of components with a selected index, designed for swipeable pager interfaces where users can jump between tabs or pages without hierarchical history. According to the Decompose source code, both use the same underlying instance management but expose different navigation contracts.

How do I prevent animation when programmatically selecting a page?

Pass PagesScrollAnimation.Disabled to the scrollAnimation parameter when invoking ChildPages. This configuration causes the composable to use state.scrollToPage(selectedIndex) instead of animateScrollToPage, resulting in instant transitions when the navigation model changes. This is useful when restoring state or responding to deep links where visual animation would be distracting.

Can I use ChildPages with custom pager implementations?

Yes, the pager parameter accepts any composable matching the ChildPagesPager typealias signature, allowing substitution of the default HorizontalPager or VerticalPager with custom implementations. The defaultHorizontalPager() and defaultVerticalPager() functions simply delegate to the official Compose pager widgets, but you can provide your own pager that handles gestures, layout, or visual effects differently while maintaining the same state synchronization contract with Decompose.

How does ChildPages handle component lifecycle during swiping?

ChildPages manages lifecycle lazily: child components are instantiated only when their page index becomes active or adjacent (depending on the pager's offscreen page limit), and they are retained in a Ref cache during recompositions to survive configuration changes. When pages are swiped away and destroyed, Decompose's standard instance management invokes the component's lifecycle callbacks according to the Lifecycle subscriptions, ensuring proper cleanup of resources and cancellation of coroutines scoped to those components.

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 →