navigation-event
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCommon guidelines
通用指南
- For architecture concepts : To understand the foundational architecture, continuous gesture event lifecycles, or class definitions of the Navigation Event library, read Navigation Event overview.
- For Android target : If compile SDK is lower than 36, set it to or higher in
36.build.gradle.kts - For Compose Android target: The project must use Jetpack Compose for Compose-specific APIs. This skill is scoped exclusively to Compose Android (Android Views and non-Compose implementations are excluded).
- For activity dispatchers : automatically implements
ComponentActivityout-of-the-box. You must use the built-inNavigationEventDispatcherOwnerwithout creating anonymous delegate owners or overriding member properties.navigationEventDispatcher - For dialog scoping : Floating windows (Compose ,
Dialog,ModalBottomSheet) automatically provide aComponentDialog. You don't need manualNavigationEventDispatcherOwnerpropagation for dialogs.CompositionLocalProvider - For parent-child dispatcher hierarchies : When scoping navigation handling to , tabbed interfaces, or nested navigation containers in Compose, use
ViewPagersto create a child owner linked to the parent. Disabling the owner (rememberNavigationEventDispatcherOwner()) automatically cascades to disable all child handlers.enabled = false - For Compose handlers : A one-to-one relationship between and handlers is strictly enforced. Never bind the same
NavigationEventStateto multiple activeNavigationEventStateinstances (NavigationBackHandler).IllegalArgumentException
- 架构概念:要了解 Navigation Event 库的基础架构、连续手势事件生命周期或类定义,请阅读 Navigation Event 概述。
- Android 目标:如果 compileSdk 低于 36,请在 中将其设置为
build.gradle.kts或更高。36 - Compose Android 目标:项目必须使用 Jetpack Compose 才能使用 Compose 专用 API。此 skill 仅限 Compose Android(不包含 Android Views 和非 Compose 实现)。
- Activity 调度器:自动实现
ComponentActivity。必须使用内置的NavigationEventDispatcherOwner,不要创建匿名的 delegate owner 或重写成员属性。navigationEventDispatcher - Dialog 作用域:浮动窗口(Compose 、
Dialog、ModalBottomSheet)会自动提供ComponentDialog。无需为 dialog 手动传播NavigationEventDispatcherOwner。CompositionLocalProvider - 父子调度器层级:在 Compose 中将导航处理范围限定到 、选项卡界面或嵌套导航容器时,使用
ViewPagers创建与父级关联的子 owner。禁用 owner(rememberNavigationEventDispatcherOwner())会自动级联禁用所有子处理器。enabled = false - Compose 处理器:严格强制 与处理器之间的一对一关系。切勿将同一个
NavigationEventState绑定到多个活动的NavigationEventState实例(会抛出NavigationBackHandler)。IllegalArgumentException
Step 1: Plan
步骤 1:规划
To complete this step, you MUST ensure the following:
- Identify the target platform : Verify the app is targeting Compose Android. If is lower than 36, set it to
compileSdkor higher in36.build.gradle.kts - Navigation check: Check if Navigation 3 is in use. If it is in use, use Navigation 3's built-in back navigation support rather than manually implementing low-level dispatchers from this skill.
- Hierarchy check : Identify host Activities, , tabbed interfaces, or nested navigation hosts that require back gesture interception or parent-child dispatcher linking.
ViewPagers - Migration check : Check if the project is migrating from back handling (,
OnBackPressedCallback,BackHandler) toonBackPresserandNavigationEvent.NavigationBackHandler - Input interception : Detect where the app is intercepting navigation events from gestures or hardware button presses requiring translation to .
NavigationEvent
要完成此步骤,必须确保以下事项:
- 识别目标平台:确认应用以 Compose Android 为目标。如果 低于 36,请在
compileSdk中将其设置为build.gradle.kts或更高。36 - 导航检查:检查是否使用了 Navigation 3。如果使用,请使用 Navigation 3 内置的后退导航支持,而不是手动实现本 skill 中的低级调度器。
- 层级检查:识别需要拦截返回手势或需要父子调度器关联的宿主 Activity、、选项卡界面或嵌套导航宿主。
ViewPagers - 迁移检查:检查项目是否正在从旧的返回处理(、
OnBackPressedCallback、BackHandler)迁移到onBackPresser和NavigationEvent。NavigationBackHandler - 输入拦截:检测应用在哪些位置拦截来自手势或硬件按钮按下的导航事件,这些事件需要转换为 。
NavigationEvent
Step 2: Set up dependencies
步骤 2:设置依赖
To complete this step, you MUST ensure the following:
- For setting up compile SDKs, declaring catalog versions, and adding dependencies, follow setup guide.
要完成此步骤,必须确保以下事项:
- 有关设置 compileSdk、声明 catalog 版本和添加依赖,请遵循 设置指南。
Step 3: Configure dispatcher and inputs
步骤 3:配置调度器和输入
To complete this step, you MUST ensure the following:
- To configure your dispatcher, leverage automatic or
ComponentActivityowner resolution.ComponentDialog - Link parent-child dispatchers in Compose following dispatcher guide.
要完成此步骤,必须确保以下事项:
- 要配置调度器,请利用自动的 或
ComponentActivityowner 解析。ComponentDialog - 在 Compose 中按照 调度器指南 关联父子调度器。
Step 4: Handle back navigation and UI transitions
步骤 4:处理返回导航和 UI 转场
To complete this step, you MUST ensure the following:
- To create navigation event handlers, integrate back gesture interception in Compose, animate UI components during swipes, and migrate from legacy back handlers, follow handle back guide.
要完成此步骤,必须确保以下事项:
- 要创建导航事件处理器、在 Compose 中集成返回手势拦截、在滑动过程中为 UI 组件添加动画,以及从旧版返回处理器迁移,请遵循 返回处理指南。
Step 5: Clean up resources
步骤 5:清理资源
[!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()
You MUST perform explicit manual cleanup only when managing custom
dispatchers or non-Compose handlers:
- Call on active handlers during teardown.
remove() - Call to temporarily disable navigation subtrees.
isEnabled = false - Call on dispatcher instances when hosting components are destroyed. Disposing a parent dispatcher automatically cascades to all child dispatchers.
dispose()
[!WARNING] 警告: Compose API 会自动执行销毁清理。使用和NavigationBackHandler等 Compose API 时,当 composable 离开组合时,处理器移除和调度器销毁会自动发生。rememberNavigationEventDispatcherOwner()
只有在管理自定义调度器或非 Compose 处理器时,必须执行显式手动清理:
- 在销毁期间对活动处理器调用 。
remove() - 调用 临时禁用导航子树。
isEnabled = false - 在宿主组件被销毁时对调度器实例调用 。销毁父调度器会自动级联到所有子调度器。
dispose()
Core troubleshooting guidelines
核心故障排查指南
1. Activity dispatcher setup (StackOverflowError recursion)
1. Activity 调度器设置(StackOverflowError 递归)
ComponentActivityNavigationEventDispatcherOwnernavigationEventDispatcherComponentActivityNavigationEventDispatcherOwnernavigationEventDispatcherRIGHT
正确
Why this is RIGHT : Compose apps use as the host.
automatically resolves the
Activity's built-in dispatcher.
ComponentActivityLocalNavigationEventDispatcherOwner.currentkotlin
// RIGHT
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationContent()
}
}
}为什么这是正确的:Compose 应用使用 作为宿主。 会自动解析 Activity 的内置调度器。
ComponentActivityLocalNavigationEventDispatcherOwner.currentkotlin
// RIGHT
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationContent()
}
}
}WRONG
错误
Why this is WRONG : Implementing directly on
and overriding with a new instance
shadows the library's extension property, causing a recursive infinite loop
crash on launch (). Creating redundant anonymous delegate
owners () is unnecessary.
NavigationEventDispatcherOwnerMainActivitynavigationEventDispatcherStackOverflowErrorobject : NavigationEventDispatcherOwnerkotlin
// WRONG
class MainActivity : ComponentActivity(), NavigationEventDispatcherOwner {
override val navigationEventDispatcher = NavigationEventDispatcher() // Shadow loop crash
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationContent()
}
}
}为什么这是错误的:直接在 上实现 并用新实例重写 会遮蔽库的扩展属性,导致启动时递归无限循环崩溃()。创建多余的匿名 delegate owner()也是不必要的。
MainActivityNavigationEventDispatcherOwnernavigationEventDispatcherStackOverflowErrorobject : NavigationEventDispatcherOwnerkotlin
// WRONG
class MainActivity : ComponentActivity(), NavigationEventDispatcherOwner {
override val navigationEventDispatcher = NavigationEventDispatcher() // Shadow loop crash
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationContent()
}
}
}2. Floating window and dialog scoping (automatic ComponentDialog owner)
2. 浮动窗口和对话框作用域(自动 ComponentDialog owner)
Floating windows (Compose , , and any window backed by
) automatically provide a .
Don't manually re-provide using
inside dialogs.
DialogModalBottomSheetComponentDialogNavigationEventDispatcherOwnerLocalNavigationEventDispatcherOwnerCompositionLocalProvider浮动窗口(Compose 、 以及任何由 支持的窗口)会自动提供 。不要在对话框内使用 手动重新提供 。
DialogModalBottomSheetComponentDialogNavigationEventDispatcherOwnerCompositionLocalProviderLocalNavigationEventDispatcherOwnerRIGHT
正确
Why this is RIGHT : handles navigation dispatchers
automatically. Compose components resolve their dispatcher owner
out-of-the-box without manual propagation.
ComponentDialogDialogkotlin
// RIGHT
@Composable
fun MyDialog(onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
onBackCompleted = onDismiss
)
}
}为什么这是正确的: 自动处理导航调度器。Compose 组件无需手动传播即可开箱即用地解析其调度器 owner。
ComponentDialogDialogkotlin
// RIGHT
@Composable
fun MyDialog(onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
onBackCompleted = onDismiss
)
}
}WRONG
错误
Why this is WRONG : Wrapping dialog content in a manual
creates redundant boilerplate and obscures the
automatic dispatcher resolution provided by .
CompositionLocalProviderComponentDialogkotlin
// 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
)
}
}
}为什么这是错误的:在手动 中包装对话框内容会创建冗余样板代码,并掩盖 提供的自动调度器解析。
CompositionLocalProviderComponentDialogkotlin
// 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
)
}
}
}3. Parent-child dispatcher hierarchy (ViewPagers
and nested navigation)
ViewPagers3. 父子调度器层级(ViewPagers
和嵌套导航)
ViewPagersWhen managing nested UI hierarchies such as , tabbed
interfaces, or custom navigation containers in Compose, use
to create a child owner linked to the
composition hierarchy. Setting on the child owner
automatically disables its dispatcher and all registered child handlers.
ViewPagersrememberNavigationEventDispatcherOwner()enabled = false在 Compose 中管理嵌套 UI 层级(如 、选项卡界面或自定义导航容器)时,使用 创建与组合层级关联的子 owner。在子 owner 上设置 会自动禁用其调度器及所有已注册的子处理器。
ViewPagersrememberNavigationEventDispatcherOwner()enabled = falseRIGHT
正确
Why this is RIGHT : Using creates a scoped child dispatcher linked to the parent from
. Providing it using
ensures non-visible tabs or pages automatically stop
intercepting back gestures without leaking handlers.
rememberNavigationEventDispatcherOwner(enabled = isSelected)LocalNavigationEventDispatcherOwner.currentCompositionLocalProviderkotlin
// 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
}
}为什么这是正确的:使用 会创建一个作用域子调度器,并从 关联到父级。使用 提供它可确保不可见的选项卡或页面自动停止拦截返回手势,而不会泄漏处理器。
rememberNavigationEventDispatcherOwner(enabled = isSelected)LocalNavigationEventDispatcherOwner.currentCompositionLocalProviderkotlin
// 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
}
}WRONG
错误
Why this is WRONG : Creating unlinked standalone dispatchers, instantiating
raw dispatchers without remembering them across recompositions, or attempting to
use non-existent methods like breaks hierarchy routing and leaves
child handlers active even when the page is inactive.
.addChild()kotlin
// 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)
}为什么这是错误的:创建未关联的独立调度器、在重组之间不进行 remember 就实例化原始调度器,或尝试使用 等不存在的方法,都会破坏层级路由,并使子处理器即使在页面处于非活动状态时也保持活动。
.addChild()kotlin
// 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)
}4. Compose multi-handler registration (IllegalArgumentException)
4. Compose 多处理器注册(IllegalArgumentException)
You must not bind the same to multiple active
instances, as this throws an
at runtime. To handle conditional workflows (such as checking for unsaved
changes versus navigating back immediately), you must register a single unified
handler and branch logic inside .
NavigationEventStateNavigationBackHandlerIllegalArgumentExceptiononBackCompleted不得将同一个 绑定到多个活动的 实例,这会在运行时抛出 。要处理条件工作流(例如检查未保存的更改与立即返回导航),必须注册一个统一的处理器并在 内进行分支逻辑处理。
NavigationEventStateNavigationBackHandlerIllegalArgumentExceptiononBackCompletedRIGHT
正确
Why this is RIGHT : Using a single with internal
branching logic inside maintains a strict 1:1 mapping between
and the handler, preventing state collisions.
NavigationBackHandleronBackCompletedNavigationEventStatekotlin
// RIGHT
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
isBackEnabled = true,
onBackCompleted = {
if (hasUnsavedChanges) {
showDiscardDialog()
} else {
onNavigateUp()
}
}
)为什么这是正确的:使用单个 并在 内进行内部分支逻辑,可保持 与处理器之间严格的 1:1 映射,防止状态冲突。
NavigationBackHandleronBackCompletedNavigationEventStatekotlin
// RIGHT
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
isBackEnabled = true,
onBackCompleted = {
if (hasUnsavedChanges) {
showDiscardDialog()
} else {
onNavigateUp()
}
}
)WRONG
错误
Why this is WRONG : Attaching multiple composables to
the same instance attempts to bind duplicate handlers to a
single state object, which throws an at runtime.
NavigationBackHandlernavigationStateIllegalArgumentExceptionkotlin
// WRONG
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
isBackEnabled = hasUnsavedChanges,
onBackCompleted = { /* Discard changes */ }
)
NavigationBackHandler(
state = navigationState,
isBackEnabled = !hasUnsavedChanges,
onBackCompleted = { /* Navigate up */ }
)为什么这是错误的:将多个 composable 附加到同一个 实例会尝试将重复处理器绑定到单个状态对象,这会在运行时抛出 。
NavigationBackHandlernavigationStateIllegalArgumentExceptionkotlin
// WRONG
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
isBackEnabled = hasUnsavedChanges,
onBackCompleted = { /* Discard changes */ }
)
NavigationBackHandler(
state = navigationState,
isBackEnabled = !hasUnsavedChanges,
onBackCompleted = { /* Navigate up */ }
)Checklist
检查清单
For Compose Android targets:
- [ ] Is compile SDK set to or higher? (If compile SDK is lower than 36, set it to
36or higher in36).build.gradle.kts - [ ] Is NOT explicitly set to
android:enableOnBackInvokedCallbackin"false"? (On API 36+, it defaults toAndroidManifest.xml; on API 33--35, ensure it is set to"true")."true" - [ ] Does the Activity rely on the built-in dispatcher owner without redundant anonymous delegate wrapping?
ComponentActivity - [ ] Do dialogs or sheets rely on automatic dispatcher resolution without redundant
ComponentDialogwrapping?CompositionLocalProvider - [ ] Are parent-child dispatcher relationships in Compose scoped using when managing nested hierarchies?
rememberNavigationEventDispatcherOwner() - [ ] Is conditional back logic handled within a single unified to avoid duplicate registration (
NavigationBackHandler)?IllegalArgumentException - [ ] Are legacy usages migrated to
BackHandlerwith predictive progress support?NavigationBackHandler - [ ] Does the project build and pass tests successfully?
对于 Compose Android 目标:
- [ ] compileSdk 是否设置为 或更高?(如果 compileSdk 低于 36,请在
36中将其设置为build.gradle.kts或更高)。36 - [ ] 在 中是否未将
AndroidManifest.xml显式设置为android:enableOnBackInvokedCallback?(在 API 36+ 上,默认值为"false";在 API 33--35 上,请确保将其设置为"true")。"true" - [ ] Activity 是否依赖内置的 调度器 owner,而没有冗余的匿名 delegate 包装?
ComponentActivity - [ ] 对话框或底部弹窗是否依赖自动的 调度器解析,而没有冗余的
ComponentDialog包装?CompositionLocalProvider - [ ] 在 Compose 中管理嵌套层级时,父子调度器关系是否使用 进行作用域限定?
rememberNavigationEventDispatcherOwner() - [ ] 条件返回逻辑是否在单个统一的 中处理,以避免重复注册(
NavigationBackHandler)?IllegalArgumentException - [ ] 传统的 用法是否迁移到支持预测性进度的
BackHandler?NavigationBackHandler - [ ] 项目是否能成功构建并通过测试?