題幹與適用情境
你維護一個包含 Activity、Fragment 與 Compose 頁面的大型 Android 應用程式。從 Android 15 開始使用者會看到系統返回預覽,但舊頁面仍透過 onBackPressed 或 KeyEvent.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 發布策略。
回答前需要釐清的問題
- 頁面使用 Fragment/Navigation Component、Compose,還是自訂 Activity back stack?這決定 callback API 與遷移點。
- 返回時是否需要確認未儲存表單?確認邏輯要隨 UI 狀態啟用,不能在手勢完成後才判斷。
- 是否要追蹤手勢進度製作自訂動畫?只需攔截時可用
BackHandler,需要進度時才用PredictiveBackHandler。 - 目標版本是否包含 Android 15?系統動畫預設展示和測試方式不同,相容層仍需覆蓋舊版本。
- 能否按 Activity 或頁面灰度?大型應用可用 Activity 層級
enableOnBackInvokedCallback控制遷移風險。
這些答案會改變方案:不需要進度就保持簡單回呼;有表單狀態就必須實作取消回復;無法一次遷移就先讓 AndroidX 頁面接管,再逐步清理舊攔截。
30 秒回答框架
「我先盤點所有舊返回攔截與頁面狀態,把 onBackPressed、KEYCODE_BACK 遷移到 OnBackPressedCallback;需要平台回呼時使用 OnBackInvokedCallback,Compose 進度動畫使用 PredictiveBackHandler。回呼只負責目前 UI 邏輯,並依可觀察的 UI 狀態啟用或停用。手勢取消必須恢復預覽前狀態,提交才執行導覽或儲存。預設或 overlay 優先級回呼會關閉系統預覽,所以我把自訂動畫和業務日誌分開,再按 Activity 灰度測試 Android 13、14、15。」
分步深入解答
1. 把返回拆成狀態機
把手勢看成 idle → preview(progress) → committed 或 idle → preview(progress) → cancelled。預覽階段只更新可逆的視覺狀態;提交階段執行導覽、關閉頁面或顯示確認;取消階段恢復快照。這樣不會把不可逆的業務寫入放在使用者仍可能鬆手取消的階段。
2. 遷移舊攔截 API
Android 官方的相容路徑是升級 AndroidX Activity,並用 OnBackPressedDispatcher 註冊 OnBackPressedCallback。停止在 Activity.onBackPressed 或 KeyEvent.KEYCODEBACK 中攔截;平台 API OnBackInvokedCallback 適用於需要直接接入系統返回的情境。KEYCODEBACK 仍有受支援用途,但不應再作為返回攔截入口。
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,避免為簡單頁面引入不必要的進度邏輯。
PredictiveBackHandler(enabled = canNavigateBack) { progress ->
try {
progress.collect { event -> renderPreview(event.progress) }
navigateBack()
} catch (cancelled: CancellationException) {
restorePreviewState()
}
}同一層級的 callback 按堆疊處理,最後加入且啟用的 callback 優先。巢狀 Compose 中,最內層的 PredictiveBackHandler 或 BackHandler 會先處理,因此要避免多個元件同時宣告模糊的全域回呼。
4. 不要吞掉系統動畫
官方文件指出,OnBackPressedCallback 或 OnBackInvokedCallback 使用 PRIORITYDEFAULT 或 PRIORITYOVERLAY 時,系統 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 的提交和取消路徑,再逐步刪除 onBackPressed 與 KEYCODE_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。