Prompt and scope
You maintain a large Android app with Activities, Fragments, and Compose screens. From Android 15, users can see system back previews, but legacy screens still intercept back with onBackPressed or KeyEvent.KEYCODE_BACK. The result is missing animation, confirmation logic at the wrong time, or state left behind after a cancelled gesture. The interviewer wants a gradual migration and test plan.
Assume Android 13 and later must remain supported and the navigation stack cannot be rewritten at once. Android recommends the backward-compatible OnBackPressedCallback, or the platform OnBackInvokedCallback when direct system integration is required. Compose uses PredictiveBackHandler when the gesture progress is needed.
What the interviewer evaluates
- Whether you model predictive back as preview followed by commit or cancellation, not as a one-shot click.
- Whether legacy interception moves to AndroidX callbacks whose enabled state follows UI state.
- Whether you separate UI animation callbacks from logging or business observers so system animation is not consumed.
- Whether you explain Compose callback ordering, cancellation restoration, and View/Fragment compatibility.
- Whether you cover Android 15 system animation, activity-level opt-out, and repeatable device tests.
A weak answer says “upgrade AndroidX and turn on a flag.” A strong answer defines callback responsibility, idempotent cancellation recovery, and an activity-by-activity rollout.
Clarifications before answering
- Does the screen use Fragments/Navigation Component, Compose, or a custom Activity back stack? That determines the migration API.
- Must back confirm an unsaved form? The callback must follow UI state before the gesture, not decide after it completes.
- Is gesture progress needed for a custom animation? Use
BackHandlerfor final interception andPredictiveBackHandleronly when progress is required. - Does the target include Android 15? System animation and testing differ, while the compatibility layer must cover older releases.
- Can the app roll out per Activity or screen? A large app can use the activity-level
enableOnBackInvokedCallbackflag.
These answers change the design: keep a simple callback when progress is unnecessary, implement restoration for form state, and migrate AndroidX screens before removing every legacy interception.
A 30-second answer framework
“I inventory every legacy back interception and bind callbacks to observable UI state. I replace onBackPressed and KEYCODE_BACK interception with OnBackPressedCallback; I use OnBackInvokedCallback only for direct platform integration, and PredictiveBackHandler in Compose when I need progress. Preview changes only reversible visuals. A committed gesture navigates or confirms; a cancelled gesture restores the starting snapshot. Default or overlay priority callbacks can suppress system animation, so UI animation and business logging use separate paths. I roll this out per Activity and test Android 13, 14, and 15.”
Step-by-step solution
1. Model back as a state machine
Treat the gesture as idle → preview(progress) → committed or idle → preview(progress) → cancelled. Preview updates reversible visuals only. Commit performs navigation, dismissal, or confirmation. Cancellation restores a snapshot. No irreversible business write belongs in the period when the user can still release the gesture.
2. Migrate legacy interception APIs
Android’s compatibility path is to upgrade AndroidX Activity and register an OnBackPressedCallback with OnBackPressedDispatcher. Stop intercepting in Activity.onBackPressed or KeyEvent.KEYCODEBACK. Use OnBackInvokedCallback when platform-level back integration is required. KEYCODEBACK still has supported uses, but it should no longer be the back interception entry point.
val confirmCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
showDiscardDialog()
}
}
onBackPressedDispatcher.addCallback(viewLifecycleOwner, confirmCallback)
formState.collect { state ->
confirmCallback.isEnabled = state.hasUnsavedChanges
}The enabled state comes from observable form state, not a late check after the callback fires. When the form is clean, disable the callback so the system or navigation component handles back.
3. Handle Compose progress and cancellation
Use PredictiveBackHandler to collect BackEventCompat when a Compose screen needs a progress-driven animation. Restore the animation and staged state when collection is cancelled or the gesture does not commit. Use BackHandler for final interception without progress so simple screens do not carry unnecessary gesture logic.
PredictiveBackHandler(enabled = canNavigateBack) { progress ->
try {
progress.collect { event -> renderPreview(event.progress) }
navigateBack()
} catch (cancelled: CancellationException) {
restorePreviewState()
}
}Callbacks are processed as a stack: the last added enabled callback handles the next gesture. In nested Compose, the innermost PredictiveBackHandler or BackHandler wins, so avoid ambiguous global callbacks.
4. Do not swallow system animation
The Android guide says an OnBackPressedCallback or OnBackInvokedCallback at PRIORITYDEFAULT or PRIORITYOVERLAY prevents predictive system animation and makes the app handle the animation. UI callbacks should control dialogs and transitions. Logging or business observation should use a non-consuming observation path rather than a high-priority callback that blocks the system.
5. Roll out per Activity
Set android:enableOnBackInvokedCallback at the application level or override it for one Activity. A large app can enable it for migrated Activities while legacy screens temporarily opt out, then remove old interception one screen at a time. Turning the flag off ignores OnBackInvokedCallback, but AndroidX OnBackPressedCallback can still run, so test the two paths separately.
6. Test commit, cancellation, and boundaries
For every screen test: release before the commit threshold; release after the threshold; form dirty and clean; nested Fragment/Compose callbacks; returning from the root Activity to system home; and Android 13, 14, and 15. Assert navigation stack, form data, final animation state, and callback enabled state, not only a screenshot.
High-quality sample answer
“I would model back as a cancellable state machine and inventory legacy interception in Activities, Fragments, and Compose. The compatibility migration uses OnBackPressedCallback; OnBackInvokedCallback is for direct platform integration; PredictiveBackHandler is only for Compose progress animations. Callback enabled state follows whether the form is dirty. Preview changes reversible visuals, cancellation restores them, and commit navigates or confirms.
“I would avoid default or overlay priority callbacks that swallow system animation and keep UI animation separate from logging. A large app rolls out enableOnBackInvokedCallback per Activity, tests Android 13, 14, and 15 for commit and cancellation, and removes onBackPressed and KEYCODE_BACK interception gradually. Acceptance checks navigation, state restoration, callback ordering, and system back-to-home behavior, not just one animation.”
Common mistakes
- Mistake: checking dirty form state only inside
handleOnBackPressed→ Why it fails: preview already happened and cancellation cannot restore correctly → Fix: enable or disable the callback from observable state. - Mistake: registering a global
PRIORITY_OVERLAYcallback everywhere → Why it fails: system predictive animation is swallowed → Fix: consume only required UI events and use non-consuming observation for logs. - Mistake: calling only
navigateBack()in Compose → Why it fails: preview state remains after cancellation → Fix: catch cancellation and restore the snapshot. - Mistake: testing only an Android 15 device → Why it fails: compatibility APIs and older behavior remain unverified → Fix: cover Android 13, 14, 15 and activity-level flags.
Follow-ups and responses
What should happen to form data when the user releases halfway?
Preview changes temporary visuals only. Keep the form model and persisted data unchanged, then restore the visual snapshot when the gesture is cancelled. Do not show a save/discard dialog for a gesture that never committed.
Why not use one global callback for every screen?
Callback priority depends on stack order and enabled state. A global callback can hide Fragment, Navigation, or Compose logic and cannot know the current screen’s unsaved state. Screen-level single-responsibility callbacks are easier to verify.
How do you log back without sacrificing system animation?
Do not use a consuming default or overlay callback for logging. For leaving the root Activity, use a system navigation observer or lifecycle signal; keep UI callbacks responsible for UI behavior.
How does a large multi-Activity app roll out the feature?
Start with a conservative application setting, then override enableOnBackInvokedCallback=true on migrated Activities. Expand only after each Activity passes commit, cancellation, nested-callback, and older-version tests.
Which Compose API is right when only final back is needed?
Use BackHandler. It expresses final interception with simpler cancellation semantics. Use PredictiveBackHandler only when BackEventCompat.progress drives an interactive animation.
References
- Android Developers: Add support for the predictive back gesture.
- Android Developers: Predictive back design.
- Android Developers: About Predictive back for Jetpack Compose.