How ChildStack Manages Back Stack Navigation State in Decompose
ChildStack maintains back stack navigation state as an immutable list of configurations where the active child represents the top of the stack and preceding elements form the history, transformed through StackNavState and mapped to component instances via the childStack factory.
In the Decompose navigation library by Arkadii Ivanov, ChildStack serves as the core data structure for representing hierarchical navigation history. Understanding how this class manages the relationship between active and inactive children is essential for building predictable, testable navigation flows in Kotlin Multiplatform applications.
The ChildStack Data Structure
The foundation of back stack management resides in [ChildStack.kt], which defines the ChildStack class as a simple but strict state holder.
ChildStack stores two critical properties:
active: Child.Created<C, T>– Represents the currently visible (top) child component and its configurationbackStack: List<Child.Created<C, T>>– Represents the history of inactive children maintained in order
The items property exposes the complete stack as a unified list via a GettingList implementation, combining backStack + active so UI layers can iterate over the full navigation history without managing indices manually.
// Conceptual structure from ChildStack.kt
data class ChildStack<out C : Any, out T : Any>(
val active: Child.Created<C, T>,
val backStack: List<Child.Created<C, T>> = emptyList()
) {
val items: List<Child.Created<C, T>> = GettingList(backStack.size + 1) { index ->
if (index == backStack.size) active else backStack[index]
}
}
Creating and Updating the Navigation Stack
Navigation transformations occur in [ChildStackFactory.kt] through the childStack factory functions. These functions initialize a children navigation source that manages the lifecycle of components based on configuration changes.
The factory accepts several key parameters:
initialStack– Supplies the starting list of configurations (ordered from bottom to top)navTransformer– Defines how to transform the currentStackNavStatewhen navigation events occur (push, pop, replace)serializer– OptionalKSerializerfor state persistence
Internally, the factory maintains a StackNavState class (defined within ChildStackFactory.kt) that holds the immutable configurations: List<C> list. Every navigation event creates a new StackNavState instance with a modified configuration list, ensuring thread-safe, predictable state updates.
// From ChildStackFactory.kt - conceptual initialization
val stack: Value<ChildStack<Config, MyComponent>> = childStack(
source = navigation,
initialStack = { listOf(HomeConfig) },
handleBackButton = true,
childFactory = { config, componentContext ->
createComponent(config, componentContext)
}
)
Mapping Configurations to ChildStack Instances
The bridge between raw configuration lists and typed component instances happens through the stateMapper parameter inside the childStack factory.
When configurations change, the navigation source creates child components for each configuration in the stack. The stateMapper receives this List<Child.Created<C, T>> and constructs the final ChildStack:
// Implementation detail from ChildStackFactory.kt
ChildStack(
active = createdChildren.last(),
backStack = createdChildren.dropLast(1),
)
This mapping ensures that:
- The active child is always the last element in the created children list (top of stack)
- The back stack contains everything preceding the active child in historical order
- The structure remains immutable; every navigation operation produces a new
ChildStackinstance
Back Button Handling
Decompose provides declarative back button management through the backTransformer parameter when handleBackButton = true.
The transformer inspects the current StackNavState size to determine if a pop operation is valid:
- If size > 1: Returns a lambda executing
dropLast(1)on the configurations list, removing the active configuration and exposing the previous one - If size == 1: Returns
null, preventing the back button from popping the root screen
// Back handling logic from ChildStackFactory.kt
backTransformer = { state ->
if (state.configurations.size > 1) {
{ state.copy(configurations = state.configurations.dropLast(1)) }
} else {
null
}
}
This approach keeps navigation logic pure and testable, with the actual back button dispatch handled by the Decompose framework.
State Persistence Across Process Death
The back stack navigation state survives process death through the optional serialization layer. When provided with a serializer, the childStack factory:
- Saves state: Serializes the
List<C>configurations into aSerializableContainerusing the providedKSerializer - Restores state: Deserializes the configuration list on recreation, passing it to
restoreStackcallback before creating components
This ensures users return to the exact same navigation depth and history after background termination, with the entire configuration stack (not just the active screen) recovered accurately.
Compose UI Integration
The Compose-specific extension in [ChildStack.kt] (Compose module) renders the stack while managing UI state retention.
The composable accepts a Value<ChildStack> and uses SaveableStateHolder to preserve the UI state (scroll positions, text input, etc.) for every child in the stack, not just the active one. When a child leaves the back stack entirely, its associated saved state is automatically purged to prevent memory leaks.
// Compose usage from ChildStack.kt (Compose extension)
@Composable
fun MyApp() {
ChildStack(
stack = stack,
animation = stackAnimation(slide()),
) { child ->
// child.instance is the active component
when (child.configuration) {
is HomeConfig -> HomeScreen(child.instance)
is DetailsConfig -> DetailsScreen(child.instance)
}
}
}
Summary
ChildStackin[ChildStack.kt]stores navigation state asactive(top) plusbackStack(history) using immutable data structureschildStackfactory in[ChildStackFactory.kt]transforms configuration lists throughStackNavStateand maps them to component instances- Back navigation uses
dropLast(1)on the configuration list whenhandleBackButtonis enabled and the stack depth exceeds one - State persistence serializes the configuration list via the provided
serializer, restoring the complete back stack across process death - Compose integration retains UI state for all children in the stack using
SaveableStateHolder, cleaning up only when children are permanently removed
Frequently Asked Questions
How is the back stack represented internally in ChildStack?
The back stack is represented as a List<Child.Created<C, T>> stored in the backStack property of the ChildStack class defined in [ChildStack.kt]. This list contains all inactive children in historical order, while the active property holds the top child separately. The items property merges both into a single iterable view for convenience.
What happens when the back button is pressed in a ChildStack?
When handleBackButton = true, the backTransformer in [ChildStackFactory.kt] checks if state.configurations.size > 1. If true, it returns a transformation that executes dropLast(1) on the configuration list, effectively removing the active configuration and restoring the previous one as active. If only one configuration remains, it returns null to prevent exiting the root screen.
Can the back stack state survive process death?
Yes. By providing a serializer parameter to the childStack factory, the list of configurations is serialized into a SerializableContainer when the app enters the background. Upon restoration, the factory uses restoreStack to recreate the exact configuration list, reconstructing the entire navigation history including the back stack depth and specific configurations.
How does Compose retain UI state for back stack entries?
The Compose extension [ChildStack.kt] uses SaveableStateHolder to retain UI state (such as scroll position or form input) for every child present in the stack. When a child moves from active to back stack, its state is preserved. When a configuration is permanently removed from the stack (popped), the associated saved state is automatically cleared to maintain memory efficiency.
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 →