You can extend the abstract class NavigationEventHandler to handle navigation
events across platforms. This class provides methods corresponding to the
lifecycle of a navigation gesture.
val myHandler = object: NavigationEventHandler<NavigationEventInfo>( initialInfo = NavigationEventInfo.None, isBackEnabled = true ) { override fun onBackStarted(event: NavigationEvent) { // Prepare for the back event } override fun onBackProgressed(event: NavigationEvent) { // Use event.progress for predictive animations } // This is the required method for final event handling override fun onBackCompleted() { // Complete the back event } override fun onBackCancelled() { // Cancel the back event } }
The addHandler function connects the handler to the dispatcher:
navigationEventDispatcher.addHandler(myHandler)
Call myHandler.remove() to remove the handler from the dispatcher:
myHandler.remove()
Handlers are invoked based on priority, and then by recency. All
PRIORITY_OVERLAY handlers are called before any PRIORITY_DEFAULT
handlers. Within each priority group, handlers are invoked in a Last-In,
First-Out (LIFO) order — the most recently added handler is called first.
Intercept back with Jetpack Compose
For Jetpack Compose, the library provides a utility composable to manage the dispatcher hierarchy.
The NavigationBackHandler composable creates a NavigationEventHandler for
its content and links it to the LocalNavigationEventDispatcherOwner. It uses
Compose's DisposableEffect to automatically call the dispatcher's dispose()
method when the composable leaves the screen, safely managing resources.
@Composable public fun NavigationBackHandler( state: NavigationEventState<out NavigationEventInfo>, isBackEnabled: Boolean = true, onBackCancelled: () -> Unit = {}, onBackCompleted: () -> Unit, ){ }
This function lets you control event handling precisely within localized UI subtrees.
@Composable fun HandlingBackWithTransitionState( onNavigateUp: () -> Unit ) { val navigationState = rememberNavigationEventState( currentInfo = NavigationEventInfo.None ) val transitionState = navigationState.transitionState // React to predictive back transition updates when (transitionState) { is NavigationEventTransitionState.InProgress -> { val progress = transitionState.latestEvent.progress // Use progress (0f..1f) to update UI during the gesture } is NavigationEventTransitionState.Idle -> { // Reset any temporary UI state if the gesture is cancelled } } NavigationBackHandler( state = navigationState, onBackCancelled = { // Called if the back gesture is cancelled }, onBackCompleted = { // Called when the back gesture fully completes onNavigateUp() } ) }
This example shows how to observe predictive back gesture updates using
NavigationEventTransitionState. The progress value can be used to
update UI elements in response to the back gesture, while handling completion
and cancellation through NavigationBackHandler.
Access the back gesture or swipe edge in Compose
Figure 1. A predictive back animation built with NavigationEvent and Compose.
To animate the screen while the user swipes back, you'll need to (a) check if
the NavigationEventTransitionState is InProgress, and (b) observe the
progress and swipe edge state with rememberNavigationEventState:
progress: A Float from0.0to1.0indicating how far the user has swiped.swipeEdge: An integer constant (EDGE_LEFTorEDGE_RIGHT) indicating where the gesture started.
The following snippet is a simplified example of how to implement a scale and shift animation:
object Routes { const val SCREEN_A = "Screen A" const val SCREEN_B = "Screen B" } class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { var state by remember { mutableStateOf(Routes.SCREEN_A) } val backEventState = rememberNavigationEventState<NavigationEventInfo>(currentInfo = NavigationEventInfo.None) when (state) { Routes.SCREEN_A -> { ScreenA(onNavigate = { state = Routes.SCREEN_B }) } else -> { if (backEventState.transitionState is NavigationEventTransitionState.InProgress) { ScreenA(onNavigate = { }) } ScreenB( backEventState = backEventState, onBackCompleted = { state = Routes.SCREEN_A } ) } } } } } @Composable fun ScreenB( backEventState: NavigationEventState<NavigationEventInfo>, onBackCompleted: () -> Unit = {}, ) { val transitionState = backEventState.transitionState val latestEvent = (transitionState as? NavigationEventTransitionState.InProgress) ?.latestEvent val backProgress = latestEvent?.progress ?: 0f val swipeEdge = latestEvent?.swipeEdge ?: NavigationEvent.EDGE_LEFT if (transitionState is NavigationEventTransitionState.InProgress) { Log.d("BackGesture", "Progress: ${transitionState.latestEvent.progress}") } else if (transitionState is NavigationEventTransitionState.Idle) { Log.d("BackGesture", "Idle") } val animatedScale by animateFloatAsState( targetValue = 1f - (backProgress * 0.1f), label = "ScaleAnimation" ) val windowInfo = LocalWindowInfo.current val density = LocalDensity.current val maxShift = remember(windowInfo, density) { val widthDp = with(density) { windowInfo.containerSize.width.toDp() } (widthDp.value / 20f) - 8 } val offsetX = when (swipeEdge) { EDGE_LEFT -> (backProgress * maxShift).dp EDGE_RIGHT -> (-backProgress * maxShift).dp else -> 0.dp } NavigationBackHandler( state = backEventState, onBackCompleted = onBackCompleted, isBackEnabled = true ) Box( modifier = Modifier .offset(x = offsetX) .scale(animatedScale) ){ // Rest of UI } }