navigation-event

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Common 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
    36
    or higher in
    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 :
    ComponentActivity
    automatically implements
    NavigationEventDispatcherOwner
    out-of-the-box. You must use the built-in
    navigationEventDispatcher
    without creating anonymous delegate owners or overriding member properties.
  • For dialog scoping : Floating windows (Compose
    Dialog
    ,
    ModalBottomSheet
    ,
    ComponentDialog
    ) automatically provide a
    NavigationEventDispatcherOwner
    . You don't need manual
    CompositionLocalProvider
    propagation for dialogs.
  • For parent-child dispatcher hierarchies : When scoping navigation handling to
    ViewPagers
    , tabbed interfaces, or nested navigation containers in Compose, use
    rememberNavigationEventDispatcherOwner()
    to create a child owner linked to the parent. Disabling the owner (
    enabled = false
    ) automatically cascades to disable all child handlers.
  • For Compose handlers : A one-to-one relationship between
    NavigationEventState
    and handlers is strictly enforced. Never bind the same
    NavigationEventState
    to multiple active
    NavigationBackHandler
    instances (
    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
    。必须使用内置的
    navigationEventDispatcher
    ,不要创建匿名的 delegate owner 或重写成员属性。
  • Dialog 作用域:浮动窗口(Compose
    Dialog
    、
    ModalBottomSheet
    、
    ComponentDialog
    )会自动提供
    NavigationEventDispatcherOwner
    。无需为 dialog 手动传播
    CompositionLocalProvider
    。
  • 父子调度器层级:在 Compose 中将导航处理范围限定到
    ViewPagers
    、选项卡界面或嵌套导航容器时,使用
    rememberNavigationEventDispatcherOwner()
    创建与父级关联的子 owner。禁用 owner(
    enabled = false
    )会自动级联禁用所有子处理器。
  • Compose 处理器:严格强制
    NavigationEventState
    与处理器之间的一对一关系。切勿将同一个
    NavigationEventState
    绑定到多个活动的
    NavigationBackHandler
    实例(会抛出
    IllegalArgumentException
    )。

Step 1: Plan

步骤 1:规划

To complete this step, you MUST ensure the following:
  1. Identify the target platform : Verify the app is targeting Compose Android. If
    compileSdk
    is lower than 36, set it to
    36
    or higher in
    build.gradle.kts
    .
  2. 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.
  3. Hierarchy check : Identify host Activities,
    ViewPagers
    , tabbed interfaces, or nested navigation hosts that require back gesture interception or parent-child dispatcher linking.
  4. Migration check : Check if the project is migrating from back handling (
    OnBackPressedCallback
    ,
    BackHandler
    ,
    onBackPresser
    ) to
    NavigationEvent
    and
    NavigationBackHandler
    .
  5. Input interception : Detect where the app is intercepting navigation events from gestures or hardware button presses requiring translation to
    NavigationEvent
    .
要完成此步骤,必须确保以下事项:
  1. 识别目标平台:确认应用以 Compose Android 为目标。如果
    compileSdk
    低于 36,请在
    build.gradle.kts
    中将其设置为
    36
    或更高。
  2. 导航检查:检查是否使用了 Navigation 3。如果使用,请使用 Navigation 3 内置的后退导航支持,而不是手动实现本 skill 中的低级调度器。
  3. 层级检查:识别需要拦截返回手势或需要父子调度器关联的宿主 Activity、
    ViewPagers
    、选项卡界面或嵌套导航宿主。
  4. 迁移检查:检查项目是否正在从旧的返回处理(
    OnBackPressedCallback
    、
    BackHandler
    、
    onBackPresser
    )迁移到
    NavigationEvent
    和
    NavigationBackHandler
    。
  5. 输入拦截:检测应用在哪些位置拦截来自手势或硬件按钮按下的导航事件,这些事件需要转换为
    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
    ComponentActivity
    or
    ComponentDialog
    owner resolution.
  • Link parent-child dispatchers in Compose following dispatcher guide.
要完成此步骤,必须确保以下事项:
  • 要配置调度器,请利用自动的
    ComponentActivity
    或
    ComponentDialog
    owner 解析。
  • 在 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 as
NavigationBackHandler
and
rememberNavigationEventDispatcherOwner()
, handler removal and dispatcher disposal occur automatically when the composable leaves the composition.
You MUST perform explicit manual cleanup only when managing custom dispatchers or non-Compose handlers:
  • Call
    remove()
    on active handlers during teardown.
  • Call
    isEnabled = false
    to temporarily disable navigation subtrees.
  • Call
    dispose()
    on dispatcher instances when hosting components are destroyed. Disposing a parent dispatcher automatically cascades to all child dispatchers.
[!WARNING] 警告: Compose API 会自动执行销毁清理。使用
NavigationBackHandler
和
rememberNavigationEventDispatcherOwner()
等 Compose API 时,当 composable 离开组合时,处理器移除和调度器销毁会自动发生。
只有在管理自定义调度器或非 Compose 处理器时,必须执行显式手动清理:
  • 在销毁期间对活动处理器调用
    remove()
    。
  • 调用
    isEnabled = false
    临时禁用导航子树。
  • 在宿主组件被销毁时对调度器实例调用
    dispose()
    。销毁父调度器会自动级联到所有子调度器。

Core troubleshooting guidelines

核心故障排查指南

1. Activity dispatcher setup (StackOverflowError recursion)

1. Activity 调度器设置(StackOverflowError 递归)

ComponentActivity
implements
NavigationEventDispatcherOwner
automatically out-of-the-box. Don't override
navigationEventDispatcher
or wrap it in an anonymous delegate owner.
ComponentActivity
开箱即用地实现了
NavigationEventDispatcherOwner
。不要重写
navigationEventDispatcher
或将其包装在匿名的 delegate owner 中。

RIGHT

正确

Why this is RIGHT : Compose apps use
ComponentActivity
as the host.
LocalNavigationEventDispatcherOwner.current
automatically resolves the Activity's built-in dispatcher.
kotlin
// RIGHT
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationContent()
        }
    }
}
<br />
为什么这是正确的:Compose 应用使用
ComponentActivity
作为宿主。
LocalNavigationEventDispatcherOwner.current
会自动解析 Activity 的内置调度器。
kotlin
// RIGHT
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationContent()
        }
    }
}
<br />

WRONG

错误

Why this is WRONG : Implementing
NavigationEventDispatcherOwner
directly on
MainActivity
and overriding
navigationEventDispatcher
with a new instance shadows the library's extension property, causing a recursive infinite loop crash on launch (
StackOverflowError
). Creating redundant anonymous delegate owners (
object : NavigationEventDispatcherOwner
) is unnecessary.
kotlin
// WRONG
class MainActivity : ComponentActivity(), NavigationEventDispatcherOwner {
    override val navigationEventDispatcher = NavigationEventDispatcher() // Shadow loop crash
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationContent()
        }
    }
}
<br />
为什么这是错误的:直接在
MainActivity
上实现
NavigationEventDispatcherOwner
并用新实例重写
navigationEventDispatcher
会遮蔽库的扩展属性,导致启动时递归无限循环崩溃(
StackOverflowError
)。创建多余的匿名 delegate owner(
object : NavigationEventDispatcherOwner
)也是不必要的。
kotlin
// WRONG
class MainActivity : ComponentActivity(), NavigationEventDispatcherOwner {
    override val navigationEventDispatcher = NavigationEventDispatcher() // Shadow loop crash
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationContent()
        }
    }
}
<br />

2. Floating window and dialog scoping (automatic ComponentDialog owner)

2. 浮动窗口和对话框作用域(自动 ComponentDialog owner)

Floating windows (Compose
Dialog
,
ModalBottomSheet
, and any window backed by
ComponentDialog
) automatically provide a
NavigationEventDispatcherOwner
. Don't manually re-provide
LocalNavigationEventDispatcherOwner
using
CompositionLocalProvider
inside dialogs.
浮动窗口(Compose
Dialog
、
ModalBottomSheet
以及任何由
ComponentDialog
支持的窗口)会自动提供
NavigationEventDispatcherOwner
。不要在对话框内使用
CompositionLocalProvider
手动重新提供
LocalNavigationEventDispatcherOwner
。

RIGHT

正确

Why this is RIGHT :
ComponentDialog
handles navigation dispatchers automatically. Compose
Dialog
components resolve their dispatcher owner out-of-the-box without manual propagation.
kotlin
// RIGHT
@Composable
fun MyDialog(onDismiss: () -> Unit) {
    Dialog(onDismissRequest = onDismiss) {
        val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
        NavigationBackHandler(
            state = navigationState,
            onBackCompleted = onDismiss
        )
    }
}
<br />
为什么这是正确的:
ComponentDialog
自动处理导航调度器。Compose
Dialog
组件无需手动传播即可开箱即用地解析其调度器 owner。
kotlin
// RIGHT
@Composable
fun MyDialog(onDismiss: () -> Unit) {
    Dialog(onDismissRequest = onDismiss) {
        val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
        NavigationBackHandler(
            state = navigationState,
            onBackCompleted = onDismiss
        )
    }
}
<br />

WRONG

错误

Why this is WRONG : Wrapping dialog content in a manual
CompositionLocalProvider
creates redundant boilerplate and obscures the automatic dispatcher resolution provided by
ComponentDialog
.
kotlin
// 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
            )
        }
    }
}
<br />
为什么这是错误的:在手动
CompositionLocalProvider
中包装对话框内容会创建冗余样板代码,并掩盖
ComponentDialog
提供的自动调度器解析。
kotlin
// 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
            )
        }
    }
}
<br />

3. Parent-child dispatcher hierarchy (
ViewPagers
and nested navigation)

3. 父子调度器层级(
ViewPagers
和嵌套导航)

When managing nested UI hierarchies such as
ViewPagers
, tabbed interfaces, or custom navigation containers in Compose, use
rememberNavigationEventDispatcherOwner()
to create a child owner linked to the composition hierarchy. Setting
enabled = false
on the child owner automatically disables its dispatcher and all registered child handlers.
在 Compose 中管理嵌套 UI 层级(如
ViewPagers
、选项卡界面或自定义导航容器)时,使用
rememberNavigationEventDispatcherOwner()
创建与组合层级关联的子 owner。在子 owner 上设置
enabled = false
会自动禁用其调度器及所有已注册的子处理器。

RIGHT

正确

Why this is RIGHT : Using
rememberNavigationEventDispatcherOwner(enabled = isSelected)
creates a scoped child dispatcher linked to the parent from
LocalNavigationEventDispatcherOwner.current
. Providing it using
CompositionLocalProvider
ensures non-visible tabs or pages automatically stop intercepting back gestures without leaking handlers.
kotlin
// 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
    }
}
<br />
为什么这是正确的:使用
rememberNavigationEventDispatcherOwner(enabled = isSelected)
会创建一个作用域子调度器,并从
LocalNavigationEventDispatcherOwner.current
关联到父级。使用
CompositionLocalProvider
提供它可确保不可见的选项卡或页面自动停止拦截返回手势,而不会泄漏处理器。
kotlin
// 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
    }
}
<br />

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
.addChild()
breaks hierarchy routing and leaves child handlers active even when the page is inactive.
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)
}
<br />
为什么这是错误的:创建未关联的独立调度器、在重组之间不进行 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)
}
<br />

4. Compose multi-handler registration (IllegalArgumentException)

4. Compose 多处理器注册(IllegalArgumentException)

You must not bind the same
NavigationEventState
to multiple active
NavigationBackHandler
instances, as this throws an
IllegalArgumentException
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
onBackCompleted
.
不得将同一个
NavigationEventState
绑定到多个活动的
NavigationBackHandler
实例,这会在运行时抛出
IllegalArgumentException
。要处理条件工作流(例如检查未保存的更改与立即返回导航),必须注册一个统一的处理器并在
onBackCompleted
内进行分支逻辑处理。

RIGHT

正确

Why this is RIGHT : Using a single
NavigationBackHandler
with internal branching logic inside
onBackCompleted
maintains a strict 1:1 mapping between
NavigationEventState
and the handler, preventing state collisions.
kotlin
// RIGHT
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
    state = navigationState,
    isBackEnabled = true,
    onBackCompleted = {
        if (hasUnsavedChanges) {
            showDiscardDialog()
        } else {
            onNavigateUp()
        }
    }
)
<br />
为什么这是正确的:使用单个
NavigationBackHandler
并在
onBackCompleted
内进行内部分支逻辑,可保持
NavigationEventState
与处理器之间严格的 1:1 映射,防止状态冲突。
kotlin
// RIGHT
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
    state = navigationState,
    isBackEnabled = true,
    onBackCompleted = {
        if (hasUnsavedChanges) {
            showDiscardDialog()
        } else {
            onNavigateUp()
        }
    }
)
<br />

WRONG

错误

Why this is WRONG : Attaching multiple
NavigationBackHandler
composables to the same
navigationState
instance attempts to bind duplicate handlers to a single state object, which throws an
IllegalArgumentException
at runtime.
kotlin
// WRONG
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
    state = navigationState,
    isBackEnabled = hasUnsavedChanges,
    onBackCompleted = { /* Discard changes */ }
)
NavigationBackHandler(
    state = navigationState,
    isBackEnabled = !hasUnsavedChanges,
    onBackCompleted = { /* Navigate up */ }
)
<br />
为什么这是错误的:将多个
NavigationBackHandler
composable 附加到同一个
navigationState
实例会尝试将重复处理器绑定到单个状态对象,这会在运行时抛出
IllegalArgumentException
。
kotlin
// WRONG
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
    state = navigationState,
    isBackEnabled = hasUnsavedChanges,
    onBackCompleted = { /* Discard changes */ }
)
NavigationBackHandler(
    state = navigationState,
    isBackEnabled = !hasUnsavedChanges,
    onBackCompleted = { /* Navigate up */ }
)
<br />

Checklist

检查清单

For Compose Android targets:
  • [ ] Is compile SDK set to
    36
    or higher? (If compile SDK is lower than 36, set it to
    36
    or higher in
    build.gradle.kts
    ).
  • [ ] Is
    android:enableOnBackInvokedCallback
    NOT explicitly set to
    "false"
    in
    AndroidManifest.xml
    ? (On API 36+, it defaults to
    "true"
    ; on API 33--35, ensure it is set to
    "true"
    ).
  • [ ] Does the Activity rely on the built-in
    ComponentActivity
    dispatcher owner without redundant anonymous delegate wrapping?
  • [ ] Do dialogs or sheets rely on automatic
    ComponentDialog
    dispatcher resolution without redundant
    CompositionLocalProvider
    wrapping?
  • [ ] Are parent-child dispatcher relationships in Compose scoped using
    rememberNavigationEventDispatcherOwner()
    when managing nested hierarchies?
  • [ ] Is conditional back logic handled within a single unified
    NavigationBackHandler
    to avoid duplicate registration (
    IllegalArgumentException
    )?
  • [ ] Are legacy
    BackHandler
    usages migrated to
    NavigationBackHandler
    with predictive progress support?
  • [ ] Does the project build and pass tests successfully?
对于 Compose Android 目标:
  • [ ] compileSdk 是否设置为
    36
    或更高?(如果 compileSdk 低于 36,请在
    build.gradle.kts
    中将其设置为
    36
    或更高)。
  • [ ] 在
    AndroidManifest.xml
    中是否未将
    android:enableOnBackInvokedCallback
    显式设置为
    "false"
    ?(在 API 36+ 上,默认值为
    "true"
    ;在 API 33--35 上,请确保将其设置为
    "true"
    )。
  • [ ] Activity 是否依赖内置的
    ComponentActivity
    调度器 owner,而没有冗余的匿名 delegate 包装?
  • [ ] 对话框或底部弹窗是否依赖自动的
    ComponentDialog
    调度器解析,而没有冗余的
    CompositionLocalProvider
    包装?
  • [ ] 在 Compose 中管理嵌套层级时,父子调度器关系是否使用
    rememberNavigationEventDispatcherOwner()
    进行作用域限定?
  • [ ] 条件返回逻辑是否在单个统一的
    NavigationBackHandler
    中处理,以避免重复注册(
    IllegalArgumentException
    )?
  • [ ] 传统的
    BackHandler
    用法是否迁移到支持预测性进度的
    NavigationBackHandler
    ?
  • [ ] 项目是否能成功构建并通过测试?