問題とスコープ
あなたはActivity、Fragment、Compose画面を持つ大規模なAndroidアプリを保守している。Android 15以降、ユーザーはシステムのバックプレビューを確認できるが、レガシー画面はonBackPressedまたはKeyEvent.KEYCODE_BACKでバックを横取りしたままである。その結果、アニメーションの欠落、誤ったタイミングでの確認ロジック、キャンセルされたジェスチャー後に残る状態といった問題が生じる。面接官は段階的な移行計画とテスト計画を求めている。
Android 13以降のサポートを維持し、ナビゲーションスタックを一度に書き直すことはできないと仮定する。Androidは後方互換性のあるOnBackPressedCallback、またはシステムとの直接統合が必要な場合はプラットフォームのOnBackInvokedCallbackを推奨している。Composeではジェスチャーの進行状況が必要な場合にPredictiveBackHandlerを使用する。
面接官が評価するポイント
- 予測型バックをワンショットのクリックではなく、プレビューに続くコミットまたはキャンセルとしてモデル化しているか。
- レガシーの横取りを、UIの状態に応じてenabledが切り替わるAndroidXコールバックへ移行しているか。
- システムアニメーションが消費されないよう、UIアニメーションのコールバックをロギングやビジネスオブザーバーから分離しているか。
- Composeのコールバック順序、キャンセル時の復元、View/Fragment互換性を説明しているか。
- Android 15のシステムアニメーション、Activityレベルのオプトアウト、および再現性のあるデバイステストをカバーしているか。
弱い回答は「AndroidXをアップグレードしてフラグをオンにする」と言うだけである。強い回答はコールバックの責務、べき等なキャンセル回復、Activityごとのロールアウトを定義する。
回答前の確認事項
- 画面はFragment/Navigation Component、Compose、またはカスタムActivityバックスタックを使用しているか?それによって移行APIが決まる。
- バックで未保存フォームの確認が必要か?コールバックはジェスチャー完了後ではなく、ジェスチャー前のUI状態に従う必要がある。
- カスタムアニメーションのためにジェスチャーの進行状況が必要か?最終的な横取りには
BackHandlerを使用し、進行状況が必要な場合のみPredictiveBackHandlerを使用する。 - ターゲットにAndroid 15は含まれるか?システムアニメーションとテストが異なり、互換レイヤーは古いリリースもカバーする必要がある。
- アプリをActivityまたは画面ごとにロールアウトできるか?大規模アプリはActivityレベルの
enableOnBackInvokedCallbackフラグを使用できる。
これらの回答によって設計が変わる。進行状況が不要な場合はシンプルなコールバックを維持し、フォーム状態の復元を実装し、すべてのレガシー横取りを削除する前にAndroidX画面を移行する。
30秒の回答フレームワーク
「私はすべてのレガシーバック横取りを棚卸しし、コールバックをオブザーバブルなUI状態にバインドする。onBackPressedとKEYCODE_BACKの横取りをOnBackPressedCallbackに置き換え、直接プラットフォーム統合にはOnBackInvokedCallbackのみを使用し、進行状況が必要な場合はComposeでPredictiveBackHandlerを使用する。プレビューは可逆的なビジュアルのみを変更する。コミットされたジェスチャーはナビゲートまたは確認を行い、キャンセルされたジェスチャーは開始時のスナップショットを復元する。defaultまたはoverlayプライオリティのコールバックはシステムアニメーションを抑制するため、UIアニメーションとビジネスロギングは別々のパスを使用する。これをActivityごとにロールアウトし、Android 13、14、15でテストする。」
ステップバイステップの解決策
1. バックをステートマシンとしてモデル化する
ジェスチャーをidle → preview(progress) → committedまたはidle → preview(progress) → cancelledとして扱う。プレビューは可逆的なビジュアルのみを更新する。コミットはナビゲーション、ディスミス、または確認を実行する。キャンセルはスナップショットを復元する。ユーザーがまだジェスチャーを離せる期間中に不可逆なビジネス書き込みを行ってはならない。
2. レガシー横取りAPIを移行する
Androidの互換パスは、AndroidX Activityをアップグレードし、OnBackPressedDispatcherでOnBackPressedCallbackを登録することである。Activity.onBackPressedまたはKeyEvent.KEYCODE_BACKでの横取りを停止する。プラットフォームレベルのバック統合が必要な場合はOnBackInvokedCallbackを使用する。KEYCODE_BACKにはまだサポートされている用途があるが、バック横取りのエントリーポイントとして使用すべきでない。
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をcollectする。コレクションがキャンセルされた場合やジェスチャーがコミットされない場合は、アニメーションとステージング状態を復元する。単純な画面に不要なジェスチャーロジックを持たせないよう、進行状況なしの最終的な横取りにはBackHandlerを使用する。
PredictiveBackHandler(enabled = canNavigateBack) { progress ->
try {
progress.collect { event -> renderPreview(event.progress) }
navigateBack()
} catch (cancelled: CancellationException) {
restorePreviewState()
}
}コールバックはスタックとして処理される。最後に追加された有効なコールバックが次のジェスチャーを処理する。ネストされたComposeでは、最も内側のPredictiveBackHandlerまたはBackHandlerが優先されるため、あいまいなグローバルコールバックは避ける。
4. システムアニメーションを消費しない
AndroidのガイドによれIば、PRIORITY_DEFAULTまたはPRIORITY_OVERLAYのOnBackPressedCallbackまたはOnBackInvokedCallbackは予測型システムアニメーションを妨げ、アプリ側でアニメーションを処理させることになる。UIコールバックはダイアログとトランジションを制御すべきである。ロギングやビジネス観察は、システムをブロックする高優先度コールバックではなく、消費しない観察パスを使用すべきである。
5. Activityごとにロールアウトする
android:enableOnBackInvokedCallbackをアプリケーションレベルで設定するか、1つのActivityに対してオーバーライドする。大規模アプリは移行済みのActivityに対して有効化しながら、レガシー画面は一時的にオプトアウトし、その後1画面ずつ古い横取りを削除できる。フラグをオフにするとOnBackInvokedCallbackは無視されるが、AndroidXのOnBackPressedCallbackは引き続き実行されるため、2つのパスを別々にテストする。
6. コミット、キャンセル、境界をテストする
すべての画面について以下をテストする。コミット閾値前のリリース、閾値後のリリース、フォームのダーティ/クリーン状態、ネストされたFragment/Composeコールバック、ルートActivityからシステムホームへの戻り、およびAndroid 13、14、15。スクリーンショットだけでなく、ナビゲーションスタック、フォームデータ、最終アニメーション状態、コールバックのenabled状態をアサートする。
高品質なサンプル回答
「私はバックをキャンセル可能なステートマシンとしてモデル化し、Activity、Fragment、Compose内のレガシー横取りを棚卸しする。互換性移行にはOnBackPressedCallbackを使用し、OnBackInvokedCallbackは直接プラットフォーム統合のため、PredictiveBackHandlerはComposeの進行状況アニメーションのみに使用する。コールバックのenabled状態はフォームがダーティかどうかに従う。プレビューは可逆的なビジュアルを変更し、キャンセルはそれらを復元し、コミットはナビゲートまたは確認を行う。
「システムアニメーションを消費するdefaultまたはoverlayプライオリティのコールバックを避け、UIアニメーションをロギングから分離する。大規模アプリはenableOnBackInvokedCallbackをActivityごとにロールアウトし、コミットとキャンセルについてAndroid 13、14、15でテストし、onBackPressedとKEYCODE_BACKの横取りを段階的に削除する。受け入れ検査では、1つのアニメーションだけでなく、ナビゲーション、状態復元、コールバック順序、システムのバックトゥホーム動作を確認する。」
よくあるミス
- ミス:
handleOnBackPressed内でのみダーティなフォーム状態をチェックする → 失敗する理由:プレビューはすでに発生しており、キャンセルが正しく復元できない → 修正:オブザーバブルな状態からコールバックを有効化または無効化する。 - ミス:グローバルな
PRIORITY_OVERLAYコールバックをあらゆる場所に登録する → 失敗する理由:システムの予測型アニメーションが消費される → 修正:必要なUIイベントのみを消費し、ログには消費しない観察を使用する。 - ミス:Composeでのみ
navigateBack()を呼び出す → 失敗する理由:キャンセル後にプレビュー状態が残る → 修正:キャンセルをキャッチしてスナップショットを復元する。 - ミス:Android 15デバイスのみでテストする → 失敗する理由:互換APIと古い動作が未検証のまま残る → 修正:Android 13、14、15およびActivityレベルのフラグをカバーする。
フォローアップと回答
ユーザーが途中でリリースした場合、フォームデータはどうなるべきか?
プレビューは一時的なビジュアルのみを変更する。フォームモデルと永続化されたデータは変更せず、ジェスチャーがキャンセルされたときにビジュアルスナップショットを復元する。コミットされなかったジェスチャーに対して保存/破棄ダイアログを表示しない。
なぜすべての画面に1つのグローバルコールバックを使用しないのか?
コールバックの優先順位はスタック順序とenabled状態に依存する。グローバルコールバックはFragment、Navigation、またはComposeのロジックを隠し、現在の画面の未保存状態を知ることができない。画面レベルの単一責任コールバックの方が検証が容易である。
システムアニメーションを犠牲にせずにバックをログに記録するにはどうすればよいか?
ロギングに消費するdefaultまたはoverlayコールバックを使用しない。ルートActivityを離れる場合は、システムナビゲーションオブザーバーまたはライフサイクルシグナルを使用し、UIコールバックはUI動作に責任を持たせる。
大規模なマルチActivityアプリでこの機能をロールアウトするにはどうすればよいか?
保守的なアプリケーション設定から始め、移行済みのActivityでenableOnBackInvokedCallback=trueをオーバーライドする。各Activityがコミット、キャンセル、ネストされたコールバック、および古いバージョンのテストに合格した後にのみ拡大する。
最終的なバックのみが必要な場合、どのCompose APIが適切か?
BackHandlerを使用する。より単純なキャンセルセマンティクスで最終的な横取りを表現する。BackEventCompat.progressがインタラクティブなアニメーションを駆動する場合のみPredictiveBackHandlerを使用する。
参考資料
- Android Developers: 予測型バックジェスチャーのサポートを追加する。
- Android Developers: 予測型バックのデザイン。
- Android Developers: Jetpack Composeにおける予測型バックについて。