How to Handle Back Button Presses with BackHandlerOwner in Decompose
Implement the BackHandlerOwner interface in your Decompose component and create a child handler using backHandler.child() to register lifecycle-aware back callbacks that integrate automatically with navigation stacks.
Decompose, the Kotlin Multiplatform lifecycle-aware component library by Arkadii Ivanov, delegates back button handling to Essenty's back-handler API. Components that need to respond to system back presses must implement BackHandlerOwner, which exposes a back handler that chains to parent handlers via ChildBackHandler. This pattern ensures that back navigation remains predictable across nested component hierarchies in Android and Compose Multiplatform applications.
Understanding BackHandlerOwner Delegation
In Decompose, component contexts delegate BackHandlerOwner to an internal back-handler instance. This delegation pattern makes the back handler available without manual wiring. For example, in DefaultJetpackComponentContext.kt, the interface is delegated to the underlying generic component context:
class DefaultJetpackComponentContext<T : GenericComponentContext<T>>(
delegate: T,
) : JetpackComponentContext,
StateKeeperOwner by delegate,
InstanceKeeperOwner by delegate,
BackHandlerOwner by delegate // ← Delegation happens here
By declaring your component as BackHandlerOwner by componentContext, you gain access to the backHandler property automatically. This handler connects to the parent's back chain, ensuring that child components receive back events only when they are active and enabled.
Creating Child Back Handlers with Priority
To handle back presses within a component, obtain a child back handler from the parent using the BackHandler.child extension function. This creates a ChildBackHandler (defined in ChildBackHandler.kt) that registers a BackCallback with the parent and mirrors its enabled state.
val childHandler = backHandler.child(
lifecycle = lifecycle, // Optional: ties enable-state to component lifecycle
priority = BackCallback.PRIORITY_DEFAULT + 1
)
The DefaultChildBackHandler.kt implementation manages the callback registration with the parent handler. When the child's BackHandler changes its enabled state—typically through a stack router—the updateParentCallbackEnabledState() method synchronizes this state with the parent's callback. Higher priority values ensure your handler receives the back event before lower-priority siblings.
Wiring Back Handling to Navigation Stacks
When creating a stack router using childStack, pass the child back handler to the navigator. The router automatically enables or disables the back callback based on the current configuration's isBackEnabled flag.
In ChildrenFactory.kt, the router updates the callback state:
onStateChanged = { newState, oldState, isBackEnabled ->
backCallback.isEnabled = isBackEnabled
onStateChanged(newState, oldState)
}
This integration means the back button is only active when the current screen configuration explicitly allows back navigation. The ChildrenNavigator receives the backHandler parameter during initialization, creating a declarative link between the navigation state and system back events.
Complete Implementation Examples
Minimal Component with Manual Back Callback
For simple screens that don't use the stack router, register a callback directly on the child handler:
class SimpleScreen(
componentContext: ComponentContext,
) : BackHandlerOwner by componentContext, ComponentContext by componentContext {
private val backHandler = backHandler.child(
lifecycle = lifecycle,
priority = BackCallback.PRIORITY_DEFAULT + 1
)
init {
backHandler.register(BackCallback(isEnabled = true) {
onBackPressed()
})
}
private fun onBackPressed() {
println("Back pressed in SimpleScreen")
}
}
Stack Router with Automatic Back Handling
Components using childStack delegate back handling to the router. The CountersComponent.kt interface declares BackHandlerOwner, while DefaultCountersComponent.kt implements the back logic:
internal class DefaultCountersComponent(
componentContext: ComponentContext,
) : CountersComponent, ComponentContext by componentContext {
private val navigation = StackNavigation<Config>()
private val _stack = childStack(
source = navigation,
serializer = Config.serializer(),
initialConfiguration = Config(index = 0, isBackEnabled = false),
childFactory = ::child,
backHandler = backHandler.child(priority = BackCallback.PRIORITY_DEFAULT + 1) // Injected here
)
override val stack: Value<ChildStack<*, CounterComponent>> = _stack
override fun onBackClicked() {
navigation.pop()
}
}
When the current configuration's isBackEnabled property is true, the router enables the BackCallback. Pressing the system back button triggers onBackClicked, which pops the navigation stack and updates the enabled state based on the new configuration.
Overriding Handler Priority
To intercept back events before sibling components, increase the priority value:
val highPriorityHandler = backHandler.child(
lifecycle = lifecycle,
priority = BackCallback.PRIORITY_DEFAULT + 10 // Higher than default
)
highPriorityHandler.register(BackCallback(isEnabled = true) {
// Handles back before lower-priority handlers
})
Summary
- Implement
BackHandlerOwnerby delegating to yourComponentContextto access the back handler automatically. - Create child handlers using
backHandler.child()with optional lifecycle binding and priority settings. - Connect to navigation by passing the child handler to
childStack; the router managesisEnabledbased on configuration flags. - Reference key files:
ChildBackHandler.ktdefines the interface,DefaultChildBackHandler.kthandles parent registration, andChildrenFactory.ktwires the callback to navigation state changes.
Frequently Asked Questions
What is the difference between BackHandler and ChildBackHandler?
BackHandler is the public API from Essenty for registering back callbacks. ChildBackHandler (defined in decompose/src/commonMain/kotlin/com/arkivanov/decompose/backhandler/ChildBackHandler.kt) is Decompose's internal extension that creates a bridge between a component's local handler and its parent's handler chain. You typically interact with BackHandler through the child() extension function, which returns a ChildBackHandler instance.
How does priority affect back button handling?
Priority determines the order in which registered callbacks receive back events. Higher integer values execute first. BackCallback.PRIORITY_DEFAULT provides the baseline; adding values (e.g., + 1 or + 10) ensures your component handles the back press before siblings with lower priority. This is useful for modal dialogs or temporary UI that should intercept the back button before underlying navigation.
Is the back handler automatically lifecycle-aware?
Yes. When creating a child handler via backHandler.child(lifecycle = lifecycle), the handler automatically enables or disables its callback based on the component's lifecycle state. When the lifecycle is destroyed, the callback unregisters from the parent, preventing memory leaks and ensuring back events only reach active components.
Can I use BackHandlerOwner in Compose Multiplatform projects?
Yes. While DefaultJetpackComponentContext.kt targets Android-specific contexts, the BackHandlerOwner interface and ChildBackHandler implementation reside in the common decompose module. This allows sharing back-button handling logic across Android, iOS, Desktop, and Web targets, with platform-specific integrations handling the actual system back button dispatch.
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 →