How Decompose Integrates with Jetpack Compose for UI Rendering

Decompose integrates with Jetpack Compose through the Children composable, which subscribes to ChildStack navigation state, preserves child state across configuration changes, and delegates rendering to user-provided content lambdas while supporting customizable StackAnimation transitions.

Decompose is a component-oriented library developed by Arkadii Ivanov that isolates business logic, navigation, and state management from the UI layer. When you need to integrate Decompose with Jetpack Compose, the library provides dedicated extension modules that bridge its ComponentContext-based architecture with Compose's declarative rendering paradigm.

Core Architecture Concepts

ComponentContext and Lifecycle Bridging

At the heart of every Decompose component is ComponentContext, which holds lifecycle, saved-state, and instance state. When running on Android, you can use JetpackComponentContext from the jetpack-component-context module to bridge with Android Jetpack APIs.

In jetpack-component-context/src/commonMain/kotlin/com/arkivanov/decompose/jetpackcomponentcontext/JetpackComponentContext.kt, the interface extends ComponentContext and adds LifecycleOwner and ViewModelStoreOwner capabilities. This allows Decompose components to host Android ViewModel instances and observe Jetpack lifecycle events while remaining testable and platform-agnostic at the business logic layer.

ChildStack as Navigation State

Navigation in Decompose is represented by ChildStack, defined in decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStack.kt. This immutable data structure contains:

  • The currently active child component
  • A back-stack of previously active configurations

The UI layer receives navigation updates through Value<ChildStack<Config, Child>>, Decompose's reactive observable type. Unlike direct Compose State, Value is platform-agnostic and manages subscription lifecycle automatically.

The Children Composable

The primary integration point between Decompose and Jetpack Compose is the Children composable, located in extensions-compose/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/stack/Children.kt.

This composable performs three critical functions:

  1. State Subscription: Converts the Value<ChildStack> into a Compose State using subscribeAsState()
  2. State Preservation: Uses rememberSaveableStateHolder() and retainStates() to maintain each child's SavedStateHandle across configuration changes
  3. Rendering Delegation: Invokes a user-provided content lambda to render the active child, wrapping it in a SaveableStateProvider
// MyComponent.kt
class MyComponent(
    componentContext: ComponentContext,
) : ComponentContext by componentContext {

    private val _stack = mutableValue(
        ChildStack(Config.Home, HomeChild(componentContext))
    )
    val stack: Value<ChildStack<Config, Child>> = _stack

    fun openDetails(id: String) {
        _stack.value = ChildStack(
            Config.Details(id),
            DetailsChild(componentContext, id)
        )
    }

    fun goBack() {
        _stack.value = _stack.value.backStack.lastOrNull()
            ?.let { ChildStack(it.configuration, it.instance) }
            ?: _stack.value
    }

    sealed class Config { 
        object Home : Config() 
        data class Details(val id: String) : Config() 
    }
    
    sealed class Child {
        data class HomeChild(val ctx: ComponentContext) : Child()
        data class DetailsChild(val ctx: ComponentContext, val id: String) : Child()
    }
}
// MyScreen.kt
@Composable
fun MyScreen(component: MyComponent) {
    Children(
        stack = component.stack,
        animation = stackAnimation(animator = fade() + scale())
    ) { child ->
        when (val c = child.instance) {
            is MyComponent.Child.HomeChild -> 
                HomeContent(onItemClick = component::openDetails)
            is MyComponent.Child.DetailsChild -> 
                DetailsContent(id = c.id, onBack = component::goBack)
        }
    }
}

Animating Navigation Transitions

StackAnimation Interface

Decompose separates animation logic from UI rendering through the StackAnimation interface, defined in extensions-compose/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/stack/animation/StackAnimation.kt.

This functional interface receives the entire ChildStack, a Modifier, and a content lambda. Implementations determine how to animate between the previous and new active children. The extensions-compose module provides factory functions like stackAnimation() that combine multiple StackAnimator instances (such as fade() and scale()) into a single animation.

Predictive Back Gesture Support

For Android 13+ predictive back gestures, Decompose provides PredictiveBackParams in extensions-compose-experimental/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/experimental/stack/animation/PredictiveBackParams.kt.

This data class connects the platform's predictive back handler with the component's navigation logic, enabling fluid animations that follow the user's finger gesture before the back action completes.

// SharedTransitionsContent.kt (from sample)
@OptIn(ExperimentalSharedTransitionApi::class, ExperimentalDecomposeApi::class)
@Composable
internal fun SharedTransitionsContent(
    component: SharedTransitionsComponent,
    modifier: Modifier = Modifier,
) {
    SharedTransitionLayout(modifier = modifier) {
        ChildStack(
            stack = component.stack,
            modifier = Modifier.fillMaxSize().background(Color.Black),
            animation = stackAnimation(
                animator = fade() + scale(),
                predictiveBackParams = {
                    PredictiveBackParams(
                        backHandler = component.backHandler,
                        onBack = component::onBack,
                        animatable = ::materialPredictiveBackAnimatable,
                    )
                },
            ),
        ) {
            when (val child = it.instance) {
                is SharedTransitionsComponent.Child.GalleryChild ->
                    GalleryContent(
                        component = child.component,
                        animatedVisibilityScope = this,
                        modifier = Modifier.fillMaxSize(),
                    )
                is SharedTransitionsComponent.Child.PhotoChild ->
                    PhotoContent(
                        component = child.component,
                        animatedVisibilityScope = this,
                        modifier = Modifier.fillMaxSize(),
                    )
            }
        }
    }
}

Bridging with Android Jetpack APIs

When targeting Android, you can leverage JetpackComponentContext to integrate Decompose with Android-specific architecture components. This interface, defined in jetpack-component-context/src/commonMain/kotlin/com/arkivanov/decompose/jetpackcomponentcontext/JetpackComponentContext.kt, extends ComponentContext to provide LifecycleOwner and ViewModelStoreOwner capabilities.

This bridge allows Decompose components to host Android ViewModel instances that survive configuration changes while remaining compatible with Compose. The extensions-compose module provides a viewModel { ... } extension function that works seamlessly with JetpackComponentContext.

// CounterComponent.kt
class CounterComponent(
    componentContext: JetpackComponentContext,
) : JetpackComponentContext by componentContext {

    // Jetpack ViewModel that survives config changes
    private val vm: CounterViewModel = viewModel { CounterViewModel() }

    val count: State<Int> = vm.counter.collectAsState()
    fun inc() = vm.inc()
}
// CounterScreen.kt
@Composable
fun CounterScreen(component: CounterComponent) {
    val count by component.count
    Column {
        Text("Count = $count")
        Button(onClick = component::inc) { Text("Increment") }
    }
}

Summary

  • Decompose isolates business logic from UI through ComponentContext, while Jetpack Compose handles declarative rendering via the Children composable.
  • The Children function in extensions-compose subscribes to ChildStack navigation state, automatically preserves child state using rememberSaveableStateHolder(), and delegates rendering to user-provided content lambdas.
  • StackAnimation provides customizable transitions between navigation states, with support for Android 13+ predictive back gestures via PredictiveBackParams.
  • JetpackComponentContext bridges Decompose with Android architecture components, allowing seamless use of ViewModel and lifecycle-aware APIs within Compose UI.

Frequently Asked Questions

How does Decompose preserve UI state across configuration changes in Jetpack Compose?

The Children composable uses rememberSaveableStateHolder() and retainStates() to maintain each child's SavedStateHandle during configuration changes. This mechanism mirrors Android's ViewModel behavior by retaining state in the ChildStack while the UI layer recomposes, ensuring that text field inputs, scroll positions, and other transient UI state survive rotation.

Can I use Android Jetpack ViewModels with Decompose components?

Yes, when using JetpackComponentContext from the jetpack-component-context module, your component implements ViewModelStoreOwner. You can then use the viewModel { ... } extension from extensions-compose to create Android ViewModel instances that survive configuration changes while remaining compatible with Compose. This approach works seamlessly on Android while gracefully degrading on non-Android targets where JetpackComponentContext is unavailable.

What is the difference between ChildStack and the Children composable?

ChildStack, defined in decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStack.kt, is an immutable data structure representing the navigation stack containing the active child and back-stack configurations. Children, located in extensions-compose/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/stack/Children.kt, is the composable function that subscribes to a Value<ChildStack>, handles state preservation, applies animations, and renders the UI using a user-provided content lambda.

How do I implement predictive back gestures with Decompose and Jetpack Compose?

Pass PredictiveBackParams to the stackAnimation factory, providing the BackHandler from your component and a materialPredictiveBackAnimatable instance. This configuration, available in the extensions-compose-experimental module, enables Android 13+ predictive back animations that follow the user's finger gesture before the navigation completes, while maintaining Decompose's navigation state management as demonstrated in the SharedTransitionsContent.kt sample.

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 →