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

> Explore how ChildPages in Decompose synchronizes Jetpack Compose pager gestures with navigation state and manages component lifecycles lazily to create seamless pager-like navigation.

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

---

**`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`](https://github.com/arkivanov/decompose/blob/main/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`](https://github.com/arkivanov/decompose/blob/main/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.

```kotlin
@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:

```kotlin
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:

```kotlin
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`](https://github.com/arkivanov/decompose/blob/main/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`](https://github.com/arkivanov/decompose/blob/main/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`](https://github.com/arkivanov/decompose/blob/main/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.