代表性面试主题

Android 应用如何迁移到 Predictive Back?

通用困难
Offer.cc 编辑团队发布 更新

题干

一个 Android 应用仍用 onBackPressed 或 KeyEvent.KEYCODE_BACK 拦截返回。请设计迁移到 Predictive Back 的方案,说明 Compose 与 View 体系的差异、取消手势如何恢复状态,以及如何避免回调吞掉系统动画。

题干与适用场景

你维护一个包含 Activity、Fragment 和 Compose 页面的大型 Android 应用。用户从 Android 15 开始会看到系统返回预览,但旧页面仍通过 onBackPressedKeyEvent.KEYCODE_BACK 拦截返回,导致动画缺失、表单确认逻辑时机错误或手势取消后状态残留。面试官要求你给出渐进迁移和测试方案。

假设应用需要兼容 Android 13 及以上,也不能一次性重写所有导航。Android 官方建议使用向后兼容的 OnBackPressedCallback,或在需要平台能力时使用 OnBackInvokedCallback;Compose 使用 PredictiveBackHandler 获取手势进度。

面试官考察点

  • 是否理解 predictive back 是“预览—提交或取消”的连续手势,而不是一次性的 back click。
  • 是否能把旧拦截点迁移到 AndroidX callback,并按 UI 状态动态启用或禁用。
  • 是否区分 UI 动画回调与业务日志回调,避免默认优先级回调吞掉系统动画。
  • 是否说明 Compose callback 栈、取消时状态回滚与 View/Fragment 的兼容路径。
  • 是否能覆盖 Android 15 系统动画、activity 级 opt-out 和可重复的真机测试。

普通回答是“升级 AndroidX 并打开开关”。强回答会说明回调的责任边界、手势取消的幂等恢复和逐 Activity 发布策略。

回答前需要澄清的问题

  1. 页面使用 Fragment/Navigation Component、Compose,还是自定义 Activity back stack?这决定 callback API 和迁移点。
  2. 返回时是否需要确认未保存表单?确认逻辑要随 UI 状态启用,不能在手势已经完成后才判断。
  3. 是否要跟踪手势进度制作自定义动画?只需拦截时可用 BackHandler,需要进度时才用 PredictiveBackHandler
  4. 目标版本是否包含 Android 15?系统动画默认展示和测试方式不同,兼容层仍需覆盖旧版本。
  5. 能否按 Activity 或页面灰度?大型应用可用 activity 级 enableOnBackInvokedCallback 控制迁移风险。

这些答案会改变方案:不需要进度就保持简单回调;有表单状态就必须实现取消回滚;无法一次迁移就先让 AndroidX 页面接管,再逐步清理旧拦截。

30 秒回答框架

“我先盘点所有旧 back 拦截和页面状态,把 onBackPressedKEYCODE_BACK 迁移到 OnBackPressedCallback;需要平台回调时使用 OnBackInvokedCallback,Compose 进度动画使用 PredictiveBackHandler。回调只负责当前 UI 逻辑,并根据可观察的 UI 状态启用或禁用。手势取消必须恢复预览前状态,提交才执行导航或保存。默认或 overlay 优先级回调会关闭系统预览,所以我把自定义动画和业务日志分开,再按 Activity 灰度测试 Android 13、14、15。”

分步骤深入解答

1. 把返回拆成状态机

把手势看成 idle → preview(progress) → committedidle → preview(progress) → cancelled。预览阶段只更新可逆的视觉状态;提交阶段执行导航、关闭页面或显示确认;取消阶段恢复快照。这样不会把不可逆的业务写入放在用户仍可能松手取消的阶段。

2. 迁移旧拦截 API

Android 官方给出的兼容路径是升级 AndroidX Activity,并用 OnBackPressedDispatcher 注册 OnBackPressedCallback。停止在 Activity.onBackPressedKeyEvent.KEYCODE_BACK 中拦截;平台 API OnBackInvokedCallback 适用于需要直接接入系统返回的场景。KEYCODE_BACK 仍有受支持用途,但不应再作为返回拦截入口。

kotlin
val confirmCallback = object : OnBackPressedCallback(false) {
    override fun handleOnBackPressed() {
        showDiscardDialog()
    }
}

onBackPressedDispatcher.addCallback(viewLifecycleOwner, confirmCallback)
formState.collect { state ->
    confirmCallback.isEnabled = state.hasUnsavedChanges
}

回调的 enabled 状态来自可观察的表单状态,而不是在回调触发后才临时读取。表单无改动时禁用它,让系统或导航组件处理返回。

3. Compose 的进度与取消

Compose 页面需要自定义进度动画时使用 PredictiveBackHandler 收集 BackEventCompat。收集被取消或手势未提交时,恢复动画和暂存状态;只需要消费最终返回时使用 BackHandler,避免为简单页面引入不必要的进度逻辑。

kotlin
PredictiveBackHandler(enabled = canNavigateBack) { progress ->
    try {
        progress.collect { event -> renderPreview(event.progress) }
        navigateBack()
    } catch (cancelled: CancellationException) {
        restorePreviewState()
    }
}

同一层级的 callback 按栈处理,最后加入且启用的 callback 优先。嵌套 Compose 中,最内层的 PredictiveBackHandlerBackHandler 会先处理,因此要避免多个组件同时声明模糊的全局回调。

4. 不要吞掉系统动画

官方文档指出,OnBackPressedCallbackOnBackInvokedCallback 使用 PRIORITY_DEFAULTPRIORITY_OVERLAY 时,系统 predictive animation 不会运行,应用必须自己处理动画。UI 回调只做显示、对话框和过渡;日志或业务观察应使用不消费返回事件的观察方式,不能用高优先级 callback 阻止系统动画。

5. Activity 级渐进发布

可以在 manifest 的 application 级设置 android:enableOnBackInvokedCallback,也可以在单个 Activity 覆盖。大型应用先让已迁移 Activity 开启,旧页面暂时关闭系统动画,再逐页替换旧拦截。关闭开关会忽略 OnBackInvokedCallback,但 AndroidX 的 OnBackPressedCallback 仍可运行,因此两条链路要分别测试。

6. 测试提交、取消和边界

每个页面至少测试:从左边缘拖到未提交位置后松手;越过提交阈值后松手;表单有改动与无改动;嵌套 Fragment/Compose callback;回到根 Activity 触发系统 back-to-home;Android 13、14、15 的兼容表现。断言导航栈、表单数据、动画终态和 callback enabled 状态,而不只截图。

高质量示范回答

“我会先把返回逻辑画成可取消状态机,并盘点 Activity、Fragment、Compose 的旧拦截点。兼容迁移使用 OnBackPressedCallback,需要系统级接入时才用 OnBackInvokedCallback;Compose 只有在要呈现手势进度时使用 PredictiveBackHandler。回调 enabled 状态绑定到表单是否有未保存修改,预览只改可逆视觉状态,取消立即恢复,提交才导航或弹确认。

“我会避免用 default 或 overlay 优先级回调吞掉系统动画,把 UI 动画和日志分离。大型应用按 Activity 灰度开启 enableOnBackInvokedCallback,先测 Android 13、14、15 的提交和取消路径,再逐步删除 onBackPressedKEYCODE_BACK 拦截。验收要检查导航栈、状态恢复、callback 栈顺序和系统 back-to-home,而不只是看一次动画。”

常见错误

  • 错误表现:在 handleOnBackPressed 中才判断表单是否修改 → 失败原因:预览阶段已经发生,取消时无法恢复 → 修正方法:用可观察状态提前启用或禁用 callback。
  • 错误表现:所有页面都注册 PRIORITY_OVERLAY 回调 → 失败原因:系统 predictive animation 被吞掉 → 修正方法:只消费必要 UI 事件,日志使用非消费观察方式。
  • 错误表现:Compose 只调用 navigateBack() 不处理取消 → 失败原因:预览状态残留 → 修正方法:捕获取消并恢复快照。
  • 错误表现:只在 Android 15 真机测试 → 失败原因:兼容 API 和旧系统行为未验证 → 修正方法:覆盖 Android 13、14、15 和 Activity 级开关。

追问及应对

用户拖动到一半松手,表单内容应该怎样处理?

预览只改变临时视觉状态,表单模型和持久化数据保持不变;捕获取消后把视觉状态恢复到手势开始时的快照,不弹出保存或丢弃确认。

为什么不能用一个全局 callback 处理所有页面?

callback 按栈和启用状态决定优先级,全局 callback 容易遮蔽 Fragment、Navigation 或 Compose 的局部逻辑,也无法准确知道当前页面是否有未保存状态。让每个页面声明单一责任回调更可验证。

如何在不牺牲系统动画的情况下记录返回事件?

不要用会消费事件的 default 或 overlay callback 做日志。对离开根 Activity 的场景,使用系统导航观察回调或 Activity 生命周期信号记录;UI callback 只负责 UI 行为。

大型多 Activity 应用如何灰度?

在 application 级保守设置,再对已迁移 Activity 覆盖 enableOnBackInvokedCallback=true。每个 Activity 完成提交、取消、嵌套回调和旧版本测试后再扩大范围。

Compose 页面只需要最终返回,不需要进度,应该用什么?

BackHandler。它表达只拦截最终返回的意图,代码和取消语义更简单;只有需要根据 BackEventCompat.progress 绘制交互动画时才用 PredictiveBackHandler

参考资料

  • Android Developers:Add support for the predictive back gesture。
  • Android Developers:Predictive back design。
  • Android Developers:About Predictive back for Jetpack Compose。

公开来源

同类题目