再见 PredictiveBackHandler:如何迁移到 Compose 中的新导航事件

0 阅读5分钟

如果你在 Compose 中使用预测性返回并已更新到 Compose Multiplatform 1.10.x,你的代码可能无法再编译。这不是 bug:PredictiveBackHandler() 已被弃用,新 API 改变了你建模"返回"手势的方式。在本文中,我将解释发生了什么变化、为什么变化以及如何逐步迁移。

PredictiveBackHandler() 已在 Compose Multiplatform 1.10.3 中被弃用。迁移涉及 NavigationBackHandler()(它包装了 NavigationEventHandler())并引入了三个关键变化:

  • state 现在是必需的 — 使用 NavigationEventInfo.None 作为初始占位符。
  • onBack 被拆分为 onBackCancelledonBackCompleted
  • 手势的进度通过 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 要求你用两个显式回调来建模:onBackCancelledonBackCompleted

进度在 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

以下是基本迁移的样子(改编自官方文档API 参考):

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 需要三件事:

  1. NavigationEventInfo 声明一个显式状态。
  2. 正确建模取消与完成。
  3. 使用 transitionState 监控进度。

作为回报,你获得了更清晰、更易测试的处理方式,在取消手势中边缘情况更少。迁移工作量低,结果是代码能更好地传达其意图。

参考资料