Loading...
Loading...
Intercept back gestures and run Predictive Back animations using the NavigationEvent (androidx.navigationevent) library in Compose Android. Handles Activity setup, parent-child dispatcher scoping in `ViewPagers` or tabs, Compose `NavigationBackHandler`, and migration from legacy `BackHandler` on SDK 36+.
npx skill4agent add android/skills navigation-event36build.gradle.ktsComponentActivityNavigationEventDispatcherOwnernavigationEventDispatcherDialogModalBottomSheetComponentDialogNavigationEventDispatcherOwnerCompositionLocalProviderViewPagersrememberNavigationEventDispatcherOwner()enabled = falseNavigationEventStateNavigationEventStateNavigationBackHandlerIllegalArgumentExceptioncompileSdk36build.gradle.ktsViewPagersOnBackPressedCallbackBackHandleronBackPresserNavigationEventNavigationBackHandlerNavigationEventComponentActivityComponentDialog[!WARNING] Warning: Compose APIs perform teardown automatically. When using Compose APIs such asandNavigationBackHandler, handler removal and dispatcher disposal occur automatically when the composable leaves the composition.rememberNavigationEventDispatcherOwner()
remove()isEnabled = falsedispose()ComponentActivityNavigationEventDispatcherOwnernavigationEventDispatcherComponentActivityLocalNavigationEventDispatcherOwner.current// RIGHT
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationContent()
}
}
}NavigationEventDispatcherOwnerMainActivitynavigationEventDispatcherStackOverflowErrorobject : NavigationEventDispatcherOwner// WRONG
class MainActivity : ComponentActivity(), NavigationEventDispatcherOwner {
override val navigationEventDispatcher = NavigationEventDispatcher() // Shadow loop crash
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationContent()
}
}
}DialogModalBottomSheetComponentDialogNavigationEventDispatcherOwnerLocalNavigationEventDispatcherOwnerCompositionLocalProviderComponentDialogDialog// RIGHT
@Composable
fun MyDialog(onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
onBackCompleted = onDismiss
)
}
}CompositionLocalProviderComponentDialog// WRONG
@Composable
fun MyDialog(onDismiss: () -> Unit) {
val dispatcherOwner = LocalNavigationEventDispatcherOwner.current!!
Dialog(onDismissRequest = onDismiss) {
// Redundant: ComponentDialog provides NavigationEventDispatcherOwner automatically
CompositionLocalProvider( LocalNavigationEventDispatcherOwner provides dispatcherOwner) {
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
onBackCompleted = onDismiss
)
}
}
}ViewPagersViewPagersrememberNavigationEventDispatcherOwner()enabled = falserememberNavigationEventDispatcherOwner(enabled = isSelected)LocalNavigationEventDispatcherOwner.currentCompositionLocalProvider// RIGHT: Scoping child navigation in a ViewPager or Tab interface
@Composable
fun TabPage(isSelected: Boolean) {
val childOwner = rememberNavigationEventDispatcherOwner(enabled = isSelected)
CompositionLocalProvider(LocalNavigationEventDispatcherOwner provides childOwner) {
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
onBackCompleted = { /* Handle page back navigation */ }
)
// Page content
}
}.addChild()// WRONG
@Composable
fun TabPage(isSelected: Boolean) {
val parentDispatcher = LocalNavigationEventDispatcherOwner.current?.navigationEventDispatcher
val childDispatcher = NavigationEventDispatcher() // Unlinked and not remembered across recompositions
// WRONG: Method does not exist
parentDispatcher?.addChild(childDispatcher)
}NavigationEventStateNavigationBackHandlerIllegalArgumentExceptiononBackCompletedNavigationBackHandleronBackCompletedNavigationEventState// RIGHT
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
isBackEnabled = true,
onBackCompleted = {
if (hasUnsavedChanges) {
showDiscardDialog()
} else {
onNavigateUp()
}
}
)NavigationBackHandlernavigationStateIllegalArgumentException// WRONG
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
isBackEnabled = hasUnsavedChanges,
onBackCompleted = { /* Discard changes */ }
)
NavigationBackHandler(
state = navigationState,
isBackEnabled = !hasUnsavedChanges,
onBackCompleted = { /* Navigate up */ }
)3636build.gradle.ktsandroid:enableOnBackInvokedCallback"false"AndroidManifest.xml"true""true"ComponentActivityComponentDialogCompositionLocalProviderrememberNavigationEventDispatcherOwner()NavigationBackHandlerIllegalArgumentExceptionBackHandlerNavigationBackHandler