mcpField
N
navigation-event

navigation-event

github.com/android开发10.0k605 次安装2 天前更新
代码仓库

关于 navigation-event

面向 Compose Android 应用的 Navigation Event API 实现指南,涵盖返回手势处理、分发器层级和旧版返回处理器迁移,并说明生命周期管理与常见配置问题。

主要功能

  1. 01Compose Android 配置检查
  2. 02分发器所有权与层级
  3. 03返回手势处理
  4. 04迁移旧版返回处理器
  5. 05处理器生命周期与清理
  6. 06排查常见错误

适用场景

  1. 01迁移 BackHandler 实现
  2. 02处理 Compose 页面中的返回手势
  3. 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.md
handle-back.md
index.md
setup.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 步:规划

要完成此步骤,必须确保以下事项:

  1. 识别目标平台:确认应用以 Compose Android 为目标平台。如果 compileSdk 低于 36,请在 build.gradle.kts 中将其设置为 36 或更高。
  2. 检查导航:检查是否正在使用 Navigation 3。如果正在使用,请采用 Navigation 3 内置的返回导航支持,而不是手动实现此技能中的底层分发器。
  3. 检查层级结构:识别需要拦截返回手势或链接父子分发器的宿主 Activity、ViewPagers、选项卡式界面或嵌套导航宿主。
  4. 检查迁移:检查项目是否正从返回处理方式(OnBackPressedCallback、BackHandler、onBackPresser)迁移到 NavigationEvent 和 NavigationBackHandler。
  5. 拦截输入:检测应用中哪些位置会拦截手势或硬件按钮按下产生的导航事件,并需要将其转换为 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?
  • [ ] 项目是否成功构建并通过测试?

评论

支持 Markdown