关于 navigation-event
面向 Compose Android 应用的 Navigation Event API 实现指南,涵盖返回手势处理、分发器层级和旧版返回处理器迁移,并说明生命周期管理与常见配置问题。
主要功能
- 01Compose Android 配置检查
- 02分发器所有权与层级
- 03返回手势处理
- 04迁移旧版返回处理器
- 05处理器生命周期与清理
- 06排查常见错误
适用场景
- 01迁移 BackHandler 实现
- 02处理 Compose 页面中的返回手势
- 03为标签页或嵌套宿主划分导航作用域
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ android/ skills --skill navigation-event安装后在 Claude Code 中输入/navigation-event确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/android/guide/navigation/navigation-event/dispatcher.mdhandle-back.mdindex.mdsetup.md详细介绍
通用指南
- 对于架构概念:若要了解 Navigation Event 库的基础架构、连续手势事件生命周期或类定义,请阅读 Navigation Event 概览。
- 对于 Android 目标平台:如果 compile SDK 低于 36,请在
build.gradle.kts中将其设置为36或更高。 - 对于 Compose Android 目标平台:项目必须使用 Jetpack Compose 才能使用 Compose 专用 API。此技能仅适用于 Compose Android(不包括 Android Views 和非 Compose 实现)。
- 对于 Activity 分发器:
ComponentActivity开箱即用地自动实现NavigationEventDispatcherOwner。必须使用内置的navigationEventDispatcher,不要创建匿名委托所有者或重写成员属性。 - 对于对话框作用域:浮动窗口(Compose
Dialog、ModalBottomSheet、ComponentDialog)会自动提供NavigationEventDispatcherOwner。无需手动通过CompositionLocalProvider为对话框传递所有者。 - 对于父子分发器层级结构:在 Compose 中为
ViewPagers、选项卡式界面或嵌套导航容器限定导航处理范围时,请使用rememberNavigationEventDispatcherOwner()创建与父所有者关联的子所有者。禁用所有者(enabled = false)会自动级联禁用所有子处理器。 - 对于 Compose 处理器:严格要求
NavigationEventState与处理器一一对应。切勿将同一个NavigationEventState绑定到多个处于活动状态的NavigationBackHandler实例(IllegalArgumentException)。
第 1 步:规划
要完成此步骤,必须确保以下事项:
- 识别目标平台:确认应用以 Compose Android 为目标平台。如果
compileSdk低于 36,请在build.gradle.kts中将其设置为36或更高。 - 检查导航:检查是否正在使用 Navigation 3。如果正在使用,请采用 Navigation 3 内置的返回导航支持,而不是手动实现此技能中的底层分发器。
- 检查层级结构:识别需要拦截返回手势或链接父子分发器的宿主 Activity、
ViewPagers、选项卡式界面或嵌套导航宿主。 - 检查迁移:检查项目是否正从返回处理方式(
OnBackPressedCallback、BackHandler、onBackPresser)迁移到NavigationEvent和NavigationBackHandler。 - 拦截输入:检测应用中哪些位置会拦截手势或硬件按钮按下产生的导航事件,并需要将其转换为
NavigationEvent。
第 2 步:设置依赖项
要完成此步骤,必须确保以下事项:
- 设置 compile SDK、声明目录版本和添加依赖项时,请遵循设置指南。
第 3 步:配置分发器和输入
要完成此步骤,必须确保以下事项:
- 配置分发器时,请利用
ComponentActivity或ComponentDialog自动解析所有者。 - 在 Compose 中链接父子分发器时,请遵循分发器指南。
第 4 步:处理返回导航和 UI 转场
要完成此步骤,必须确保以下事项:
- 创建导航事件处理器、在 Compose 中集成返回手势拦截、在滑动期间为 UI 组件添加动画,以及从旧版返回处理器迁移时,请遵循返回处理指南。
步骤 5:清理资源
警告
警告: Compose API 会自动执行清理。使用
NavigationBackHandler和rememberNavigationEventDispatcherOwner()等 Compose API 时,当可组合项离开组合时,会自动移除处理器并释放分发器。
只有在管理自定义分发器或非 Compose 处理器时,才必须手动清理:
- 在清理期间对活动处理器调用
remove()。 - 调用
isEnabled = false临时禁用导航子树。 - 在宿主组件销毁时,对分发器实例调用
dispose()。释放父分发器会自动级联释放所有子分发器。
核心故障排查指南
1. Activity 分发器设置(StackOverflowError 递归)
ComponentActivity 开箱即用地实现了 NavigationEventDispatcherOwner。不要覆盖 navigationEventDispatcher,也不要将其包装在匿名委托所有者中。
正确
为什么这样做是正确的:Compose 应用使用 ComponentActivity 作为宿主。
LocalNavigationEventDispatcherOwner.current 会自动解析到 Activity 的内置分发器。
// RIGHT
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationContent()
}
}
}
错误
为什么这样做是错误的:直接在
MainActivity 上实现 NavigationEventDispatcherOwner,并通过新实例覆盖
navigationEventDispatcher,会遮蔽库的扩展属性,导致启动时发生无限递归崩溃
(StackOverflowError)。创建多余的匿名委托所有者(object : NavigationEventDispatcherOwner)没有必要。
// WRONG
class MainActivity : ComponentActivity(), NavigationEventDispatcherOwner {
override val navigationEventDispatcher = NavigationEventDispatcher() // Shadow loop crash
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationContent()
}
}
}
2. 浮动窗口和对话框作用域(自动创建 ComponentDialog 所有者)
浮动窗口(Compose Dialog、ModalBottomSheet 以及任何由
ComponentDialog 支持的窗口)会自动提供 NavigationEventDispatcherOwner。
不要在对话框内使用 CompositionLocalProvider 手动重新提供
LocalNavigationEventDispatcherOwner。
正确
为什么这样做是正确的:ComponentDialog 会自动处理导航分发器。
Compose Dialog 组件开箱即用地解析其分发器所有者,无需手动传递。
// RIGHT
@Composable
fun MyDialog(onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
onBackCompleted = onDismiss
)
}
}
错误
为什么这样做是错误的:用手动的
CompositionLocalProvider 包装对话框内容会产生多余的样板代码,并掩盖
ComponentDialog 提供的自动分发器解析机制。
// 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. 父子分发器层级(ViewPagers 和嵌套导航)
在 Compose 中管理 ViewPagers、分页界面或自定义导航容器等嵌套 UI 层级时,使用
rememberNavigationEventDispatcherOwner() 创建与组合层级关联的子所有者。在子所有者上设置 enabled = false,会自动禁用其分发器及所有已注册的子处理器。
正确
为什么这样做是正确的:使用 rememberNavigationEventDispatcherOwner(enabled = isSelected) 会创建一个有作用域的子分发器,并将其关联到
LocalNavigationEventDispatcherOwner.current 中的父分发器。通过
CompositionLocalProvider 提供该分发器,可确保不可见的标签页或页面自动停止
拦截返回手势,且不会泄漏处理器。
// 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)
}
4. Compose 多处理器注册(IllegalArgumentException)
不得将同一个 NavigationEventState 绑定到多个活动的
NavigationBackHandler 实例,否则运行时会抛出 IllegalArgumentException。
要处理条件工作流(例如检查是否存在未保存的更改,或立即返回导航),必须注册一个统一的处理器,并在 onBackCompleted 中分支处理逻辑。
正确
正确原因:在 onBackCompleted 中使用一个 NavigationBackHandler 并在内部进行分支处理,可确保 NavigationEventState 与处理器严格一一对应,避免状态冲突。
// RIGHT
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
isBackEnabled = true,
onBackCompleted = {
if (hasUnsavedChanges) {
showDiscardDialog()
} else {
onNavigateUp()
}
}
)
错误
错误原因:将多个 NavigationBackHandler 可组合项附加到同一个 navigationState 实例,会尝试将重复的处理器绑定到同一个状态对象,从而在运行时抛出 IllegalArgumentException。
// WRONG
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
isBackEnabled = hasUnsavedChanges,
onBackCompleted = { /* Discard changes */ }
)
NavigationBackHandler(
state = navigationState,
isBackEnabled = !hasUnsavedChanges,
onBackCompleted = { /* Navigate up */ }
)
检查清单
针对 Compose Android 目标平台:
- [ ] 编译 SDK 是否设为
36或更高版本?(如果编译 SDK 低于 36,请在build.gradle.kts中将其设为36或更高版本。) - [ ]
AndroidManifest.xml中是否未将android:enableOnBackInvokedCallback显式设为"false"?(在 API 36 及更高版本中,该值默认为"true";在 API 33--35 中,请确保将其设为"true"。) - [ ] Activity 是否依赖内置的
ComponentActivity分发器所有者,而没有多余地使用匿名委托进行封装? - [ ] 对话框或底部弹窗是否依赖自动解析
ComponentDialog分发器,而没有多余地使用CompositionLocalProvider进行封装? - [ ] 管理嵌套层级时,Compose 中的父子分发器关系是否使用
rememberNavigationEventDispatcherOwner()进行作用域限定? - [ ] 条件式返回逻辑是否在单个统一的
NavigationBackHandler中处理,以避免重复注册(IllegalArgumentException)? - [ ] 旧版
BackHandler用法是否已迁移到支持预测性进度的NavigationBackHandler? - [ ] 项目是否成功构建并通过测试?