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:
currentPageis set toselectedIndex(clamped to≥ 0to prevent illegal indexes when the list is empty)pageCountdynamically readspages.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→ Callsstate.scrollToPage(selectedIndex)for instant jumpsDefault→ Callsstate.animateScrollToPage(selectedIndex)using Compose's default animation curveCustom→ Callsstate.animateScrollToPage(selectedIndex, animationSpec = scrollAnimation.spec)with a user-providedAnimationSpec<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
modifierfor layout control - The derived
state - A stable
keyderived frompages.items[it]to prevent unnecessary recompositions - A
pageContentlambda
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
ChildPagessynchronizes 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 viaonPageSelectedcallbacks. - Lifecycle management creates child components lazily and caches them through recompositions using
Ref, ensuring optimal performance. - Customization supports horizontal or vertical orientations through
defaultHorizontalPager()anddefaultVerticalPager(), plus configurable animations viaPagesScrollAnimation.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →