如果你在 Compose 中使用预测性返回并已更新到 Compose Multiplatform 1.10.x,你的代码可能无法再编译。这不是 bug:PredictiveBackHandler() 已被弃用,新 API 改变了你建模"返回"手势的方式。在本文中,我将解释发生了什么变化、为什么变化以及如何逐步迁移。
PredictiveBackHandler() 已在 Compose Multiplatform 1.10.3 中被弃用。迁移涉及 NavigationBackHandler()(它包装了 NavigationEventHandler())并引入了三个关键变化:
state现在是必需的 — 使用NavigationEventInfo.None作为初始占位符。onBack被拆分为onBackCancelled和onBackCompleted。- 手势的进度通过
NavigationEventState.transitionState跟踪。
背景:什么是预测性返回以及为什么它很重要
在 Android 上,返回手势不再是一个即时事件,而是变成了一个渐进式过渡:当用户滑动时,UI 可以根据手势的进度做出动画响应。
这直接影响三种场景:
- 在返回手势期间具有自定义动画的屏幕。
- 具有过渡状态的 UI(例如,随着手势进行而"剥离"的面板)。
- 需要区分取消和完成的导航集成。
从 Compose Multiplatform 1.10.x(从 1.10.0-beta01 开始),PredictiveBackHandler() 被弃用,转而使用与 Navigation 3 对齐的 Navigation Event 库(org.jetbrains.androidx.navigationevent:navigationevent-compose)。
API 中发生了什么变化以及为什么会破坏你的代码
新 API 引入了三个基本变化,可能会迫使你重写处理程序:
state 是强制性的
- 以前,你可以在不声明状态的情况下挂钩到进度
flow。 - 现在你需要一个带有导航上下文(
NavigationEventInfo)的NavigationEventState。 - 如果你还没有有用的数据要存储,使用
NavigationEventInfo.None作为占位符。
取消和完成是独立的回调
- 以前,这是通过
collect调用周围的try/catch块处理的——这是一种反模式。 - 现在 API 要求你用两个显式回调来建模:
onBackCancelled和onBackCompleted。
进度在 transitionState 中跟踪
- 物理手势实时更新
transitionState。 - 在
InProgress状态期间,你可以读取latestEvent.progress来为你的 UI 添加动画。
旧模式:PredictiveBackHandler()
最常见的反模式是将三个应该分离的职责混合到一个块中:进度动画、副作用(弹出返回栈)和取消检测。
PredictiveBackHandler(enabled = true) { progress ->
try {
progress.collect { event ->
// Animate based on event.progress
}
// Gesture completed
} catch (e: Exception) {
// Cancelled gesture
}
}
为什么这会有问题?
- 你使用
try/catch作为流程控制,这使代码难以阅读和测试。 - 重复触发的风险:多次完成导航。
- 如果手势在中途被取消,很难保证视觉状态的一致性。
新模式:NavigationBackHandler() + NavigationEventInfo
val navState = rememberNavigationEventState(NavigationEventInfo.None)
NavigationBackHandler(
state = navState,
isBackEnabled = true,
onBackCancelled = {
// Cancelled gesture: return to stable state
},
onBackCompleted = {
// Gesture completed: execute "navigateUp/pop"
}
)
LaunchedEffect(navState.transitionState) {
val transitionState = navState.transitionState
if (transitionState is NavigationEventTransitionState.InProgress) {
val progress = transitionState.latestEvent.progress
// Animate according to progress
}
}
这里重要的是:
- 处理程序要求你显式声明状态(
navState)。 - API 清晰地区分手势的两种可能结果。
- 进度跟踪解耦到一个
LaunchedEffect中,与导航逻辑分离。
迁移清单
如果你的代码中已有 PredictiveBackHandler(),请按以下步骤操作:
- 将
PredictiveBackHandler()替换为NavigationBackHandler()。 - 使用
rememberNavigationEventState(...)创建状态。 - 如果你还没有导航上下文,从
NavigationEventInfo.None开始。 - 将进度动画移到
transitionState观察者中。 - 将最终逻辑拆分为两个回调:
onBackCompleted→ 实际导航(pop/back)。onBackCancelled→ 回滚任何瞬态。
实际示例:在手势期间动画滚动屏幕
让我们看一个具体案例。假设你想在返回手势进行时将屏幕稍微向右滚动:
@Composable
fun ScreenWithPredictiveBack(
onNavigateBack: () -> Unit,
) {
val navState = rememberNavigationEventState(NavigationEventInfo.None)
var offsetPx by remember { mutableStateOf(0f) }
NavigationBackHandler(
state = navState,
isBackEnabled = true,
onBackCancelled = {
// Return to stable state
offsetPx = 0f
},
onBackCompleted = {
// Confirm navigation
onNavigateBack()
}
)
LaunchedEffect(navState.transitionState) {
val ts = navState.transitionState
if (ts is NavigationEventTransitionState.InProgress) {
offsetPx = ts.latestEvent.progress * 40f
}
}
Box(
modifier = Modifier
.offset { IntOffset(offsetPx.roundToInt(), 0) }
.fillMaxSize()
) {
// Content
}
}
为什么这种方法更健壮?
- 进度仅在手势活跃期间更新。
onBackCancelled在不导航的情况下恢复视觉状态。onBackCompleted仅执行一次 pop,消除了重复触发的风险。
迁移时的常见错误
注意这些陷阱:
- 未在
**onBackCancelled**中重置状态 → 取消手势后 UI 保持"不同步"或处于中间状态。 - 在
**transitionState**观察者内部导航 → 导致过早或重复的 pop。 - 在
InProgress期间无控制地修改全局状态 → 产生不必要的重组。将修改限制在动画严格必要的属性上。 - 未能隔离副作用 →
onBackCompleted应该是你"提交"导航的唯一点。
生产建议
如果你希望你的实现能够良好扩展:
- 将手势视为两阶段过渡:预览(进行中)和提交/回滚(完成/取消)。
- 永远不要使用异常进行流程控制。 这是 API 更改的主要原因。
- 在
onBackCancelled中集中回滚,如果你的 UI 有多个过渡状态。 - 保持
**transitionState**观察者专注:它的职责是读取和动画,而不是导航。
结论
PredictiveBackHandler() 完成了它的使命,但它混合了一些职责,在规模化时会导致微妙的 bug。迁移到 NavigationEvent 需要三件事:
- 用
NavigationEventInfo声明一个显式状态。 - 正确建模取消与完成。
- 使用
transitionState监控进度。
作为回报,你获得了更清晰、更易测试的处理方式,在取消手势中边缘情况更少。迁移工作量低,结果是代码能更好地传达其意图。
参考资料
- Compose Multiplatform 1.10 新特性 — Kotlin 文档
- NavigationEventHandler API 参考 — Android 开发者
- Navigation Event 发布记录 — AndroidX
- Compose Multiplatform 1.10.0 博客文章 — JetBrains 博客