feat(core): back button event on Android, closes #8142#19
Conversation
WalkthroughThe PR introduces Android back button press event handling by adding a new AppPlugin that detects back button presses, emits corresponding events to JavaScript listeners, and provides an exit command. Supporting changes include TypeScript APIs, new permissions, updated build configuration, and a minor dependency version bump. Changes
Sequence DiagramsequenceDiagram
actor User
participant Android as Android System
participant Plugin as AppPlugin
participant WebView as WebView
participant JS as JavaScript Handler
User->>Android: Press back button
Android->>Plugin: OnBackPressedCallback invoked
Plugin->>Plugin: Check for back-button listener
alt Listener registered
Plugin->>Plugin: Build JSObject with canGoBack
Plugin->>JS: Emit BACK_BUTTON_EVENT
JS->>JS: Execute onBackButtonPress handler
else No listener registered
Plugin->>WebView: goBack() if canGoBack
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt`:
- Line 46: The current direct cast (activity as AppCompatActivity) before
calling onBackPressedDispatcher.addCallback is unsafe and can throw
ClassCastException; update AppPlugin.kt to perform a safe type check (e.g., use
an instanceof/is-check or a safe cast) against AppCompatActivity before calling
onBackPressedDispatcher.addCallback(activity, callback) and only register the
callback when the activity is an AppCompatActivity (otherwise skip registration
or log/handle gracefully) so the back-button behavior falls back to the system
when the activity is not AppCompatActivity.
- Around line 33-37: Replace the deprecated call to Activity.onBackPressed() in
AppPlugin with the Activity's OnBackPressedDispatcher by invoking
activity.onBackPressedDispatcher.onBackPressed(); keep the existing
disable/re-enable pattern around the call (this.isEnabled = false ...
this.isEnabled = true) to avoid recursion; locate the call site referencing
this@AppPlugin.activity.onBackPressed() and swap it to use
this@AppPlugin.activity.onBackPressedDispatcher.onBackPressed() while preserving
surrounding logic in the AppPlugin class.
In `@crates/tauri/src/app/plugin.rs`:
- Around line 135-143: The setup closure is attempting to dereference a
reference to a PluginHandle which is not Copy; remove the intermediate reference
and pass the owned handle (or clone it) directly into AppPlugin. Locate the call
to register_android_plugin and the AppPlugin construction inside the .setup
closure and replace the pattern let handle = _api.register_android_plugin(...)?;
let handle_ref = &handle; _app.manage(AppPlugin(*handle_ref)); with a direct
move or clone into _app.manage (e.g., _app.manage(AppPlugin(handle)) or
_app.manage(AppPlugin(handle.clone()))), since PluginHandle implements Clone but
not Copy.
🧹 Nitpick comments (1)
packages/api/src/app.ts (1)
255-275: Add@sinceand@exampletags to match API documentation conventions.Other functions in this file include
@sinceversion tags and@examplecode blocks. The newonBackButtonPressfunction should follow the same convention for consistency.📝 Suggested documentation improvement
/** * Listens to the backButton event on Android. - * `@param` handler + * + * `@example` + * ```typescript + * import { onBackButtonPress } from '@tauri-apps/api/app'; + * const unlisten = await onBackButtonPress((payload) => { + * if (payload.canGoBack) { + * // handle back navigation + * } + * }); + * // later, to stop listening + * unlisten.unregister(); + * ``` + * + * `@param` handler - The callback function to handle back button press events. + * `@returns` A promise resolving to a listener that can be used to unregister. + * + * `@since` 2.x.0 */ async function onBackButtonPress(
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (12)
.changes/android-app-plugin.md.changes/back-button-press-api.mdcrates/tauri-runtime-wry/Cargo.tomlcrates/tauri/build.rscrates/tauri/mobile/android-codegen/TauriActivity.ktcrates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.ktcrates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.ktcrates/tauri/permissions/app/autogenerated/reference.mdcrates/tauri/scripts/bundle.global.jscrates/tauri/src/app/plugin.rscrates/tauri/src/path/android.rspackages/api/src/app.ts
🧰 Additional context used
🧬 Code graph analysis (2)
packages/api/src/app.ts (1)
packages/api/src/core.ts (2)
PluginListener(347-347)addPluginListener(348-348)
crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt (1)
crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt (2)
hasListener(149-151)trigger(129-137)
🔇 Additional comments (14)
crates/tauri/scripts/bundle.global.js (1)
1-1: LGTM!The minified bundle correctly includes the new
onBackButtonPressAPI for Android back button handling. As this is auto-generated build output, the changes align with the PR's feature additions.crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt (1)
149-151: LGTM!The
hasListenerhelper method is cleanly implemented using idiomatic Kotlin withisNullOrEmpty(). This enables AppPlugin to check for registered back-button listeners before deciding whether to emit events or trigger system back behavior.crates/tauri/src/path/android.rs (1)
7-7: LGTM!Clean removal of unused
OsStrimport. The remainingPathandPathBufimports are properly utilized in the file..changes/back-button-press-api.md (1)
1-5: LGTM!The changeset correctly documents the new back button API as a minor feature addition to
@tauri-apps/api. The description accurately reflects the PR's changes.crates/tauri/mobile/android-codegen/TauriActivity.kt (1)
15-15: Back navigation fallback is properly implemented—no breaking change risk.The
handleBackNavigation = truedefault is safe. When no JavaScript listener is registered for the back-button event, AppPlugin gracefully falls back to standard behavior: it callswebView.goBack()if the web view can navigate back, or otherwise triggers the system's defaultonBackPressed(). Only when a JavaScript listener is registered does the event get emitted. This maintains backward compatibility with existing apps.crates/tauri-runtime-wry/Cargo.toml (1)
20-25: The wry 0.53.4 version bump is justified and appropriate.The version exists and includes Android back-navigation handling improvements (added flag to opt out of automatic back navigation handling via WryActivity#handleBackNavigation), which directly supports the Android runtime requirements. The update also includes Linux improvements for WebKitGTK compatibility.
.changes/android-app-plugin.md (1)
1-5: LGTM!The changelog entry correctly documents the new mobile app plugin feature as a minor feature addition.
crates/tauri/build.rs (1)
167-168: LGTM!The new
register_listenerandremove_listenercommands are correctly added to thecore:appplugin with default enabled, following the existing pattern for command registration.crates/tauri/permissions/app/autogenerated/reference.md (2)
12-13: LGTM!The new default permissions for
allow-register-listenerandallow-remove-listenerare correctly added, aligning with the command registrations in build.rs.
209-282: LGTM!The permission table entries for the new listener commands follow the existing documentation structure consistently.
crates/tauri/src/app/plugin.rs (1)
147-148: LGTM!The Android-only
AppPluginwrapper struct is appropriately scoped with#[cfg(target_os = "android")]and provides the necessary type to manage the plugin handle.packages/api/src/app.ts (2)
5-5: LGTM!The imports for
addPluginListenerandPluginListenerfrom./coreare correctly added to support the new back button listener functionality.
289-291: LGTM!The exports correctly include both the
OnBackButtonPressPayloadtype and theonBackButtonPressfunction.crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt (1)
49-53: LGTM!The
exitcommand correctly finishes the activity and resolves the invoke.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| } else { | ||
| this.isEnabled = false | ||
| this@AppPlugin.activity.onBackPressed() | ||
| this.isEnabled = true | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Android onBackPressed deprecated API level Android 13 OnBackPressedDispatcher
💡 Result:
Short answer
- ComponentActivity.onBackPressed() is deprecated (AndroidX) — use OnBackPressedDispatcher with OnBackPressedCallback instead. [1][2][3]
Details
- The Activity API was updated to route back events through an OnBackPressedDispatcher that invokes one or more OnBackPressedCallback instances; ComponentActivity.onBackPressed() is marked deprecated in favor of that dispatcher. [1][2]
- On Android 13 (API 33) the activity support libraries also expose integration with the platform’s predictive back (OnBackInvokedDispatcher) via setOnBackInvokedDispatcher for proper behavior on SDK 33+. Use the androidx OnBackPressedDispatcher APIs (addCallback with a LifecycleOwner) to handle back navigation and lifecycle automatically. [2][1][3]
Example (Kotlin)
- requireActivity().onBackPressedDispatcher.addCallback(this) { /* handle back */ } . [2][3]
Sources
[1] ComponentActivity (AndroidX) — deprecation note.
[2] OnBackPressedDispatcher (AndroidX) — usage and addCallback.
[3] Android Developers guide: Provide custom back navigation (recommendation to replace onBackPressed with OnBackPressedCallback).
🌐 Web query:
Android Activity onBackPressed() deprecated replacement best practices
💡 Result:
Short answer
- Don’t override Activity.onBackPressed() (deprecated). Use the Jetpack OnBackPressedDispatcher + OnBackPressedCallback for most apps, and for Android 13+ (API 33+) use the platform OnBackInvokedDispatcher/OnBackInvokedCallback or the AndroidX bridge. Register callbacks lifecycle‑aware and enable/disable them instead of removing. [1][2][3]
Why and when
- OnBackPressedDispatcher (androidx.activity) is lifecycle‑aware and is the recommended cross‑API solution (works with Fragments, ViewModels, avoids leaks). Use it by default. [1]
- OnBackInvokedDispatcher / OnBackInvokedCallback (platform, API 33+) replaces Activity.onBackPressed at the OS level and provides priority/animation hooks; AndroidX can be wired to it so your code works across API levels. [2][3]
Best practices (concise)
- Use OnBackPressedDispatcher.addCallback(owner, callback) so the callback follows the LifecycleOwner (use viewLifecycleOwner in Fragments). [1]
- Prefer enabling/disabling an OnBackPressedCallback rather than removing it. [1]
- If you target API 33+, either register an OnBackInvokedCallback for platform behavior or rely on the AndroidX dispatcher which can be set to use the platform dispatcher (AndroidX 1.6+). [1][2]
- Keep back handling focused: let UI overlays (dialogs, drawers) register higher‑priority callbacks and return control to the default flow when appropriate. [2]
Minimal examples
-
Activity (Kotlin, Jetpack):
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { /* custom */ }
}
onBackPressedDispatcher.addCallback(this, callback) // lifecycle-aware. [1] -
Fragment (Kotlin):
val cb = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { /* handle or call requireActivity().onBackPressedDispatcher.onBackPressed() */ }
}
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, cb) // use viewLifecycleOwner. [1]
Docs / references
- OnBackPressedDispatcher / OnBackPressedCallback (AndroidX). [1]
- OnBackInvokedDispatcher (platform API level 33+) — priority and window-level callbacks. [2]
- OnBackInvokedCallback (platform replacement for Activity.onBackPressed()). [3]
If you want, I can paste a copy‑pasteable Kotlin sample showing AndroidX + API‑33 bridging (setOnBackInvokedDispatcher) and explain version checks.
🏁 Script executed:
# Let's examine the full context of the code in question
cd /tmp && git clone --depth 1 https://github.com/tauri-apps/tauri.git 2>/dev/null || true
find . -name "AppPlugin.kt" -type f 2>/dev/null | head -5Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 156
🏁 Script executed:
# Get the full context of AppPlugin.kt around lines 33-37
cat -n /tmp/tauri/crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt | head -60Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 2002
🏁 Script executed:
# Let's check if there are other methods or patterns used in the codebase for back press handling
rg "onBackPressed|OnBackPressedDispatcher|OnBackPressedCallback" crates/tauri/mobile/android --type kotlin -B2 -A2Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 2002
Replace deprecated onBackPressed() with OnBackPressedDispatcher.onBackPressed().
The Activity.onBackPressed() method is deprecated in AndroidX. The modern approach is to call onBackPressedDispatcher.onBackPressed(), which properly delegates to the next callback in the dispatcher queue or the system's default back handling. The disable/re-enable pattern is still needed here to prevent callback recursion.
🔧 Suggested fix
} else {
this.isEnabled = false
- this@AppPlugin.activity.onBackPressed()
+ (this@AppPlugin.activity as AppCompatActivity).onBackPressedDispatcher.onBackPressed()
this.isEnabled = true
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } else { | |
| this.isEnabled = false | |
| this@AppPlugin.activity.onBackPressed() | |
| this.isEnabled = true | |
| } | |
| } else { | |
| this.isEnabled = false | |
| (this@AppPlugin.activity as AppCompatActivity).onBackPressedDispatcher.onBackPressed() | |
| this.isEnabled = true | |
| } |
🤖 Prompt for AI Agents
In `@crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt` around
lines 33 - 37, Replace the deprecated call to Activity.onBackPressed() in
AppPlugin with the Activity's OnBackPressedDispatcher by invoking
activity.onBackPressedDispatcher.onBackPressed(); keep the existing
disable/re-enable pattern around the call (this.isEnabled = false ...
this.isEnabled = true) to avoid recursion; locate the call site referencing
this@AppPlugin.activity.onBackPressed() and swap it to use
this@AppPlugin.activity.onBackPressedDispatcher.onBackPressed() while preserving
surrounding logic in the AppPlugin class.
| } | ||
| } | ||
| } | ||
| (activity as AppCompatActivity).onBackPressedDispatcher.addCallback(activity, callback) |
There was a problem hiding this comment.
Unsafe cast to AppCompatActivity may crash at runtime.
The cast (activity as AppCompatActivity) will throw ClassCastException if the activity is not an AppCompatActivity. Consider adding a type check or documenting this requirement.
🔧 Suggested defensive check
- (activity as AppCompatActivity).onBackPressedDispatcher.addCallback(activity, callback)
+ if (activity is AppCompatActivity) {
+ activity.onBackPressedDispatcher.addCallback(activity, callback)
+ }Note: If the callback isn't registered when activity is not AppCompatActivity, the back button behavior will fall back to the default system handling, which may be acceptable depending on the use case.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| (activity as AppCompatActivity).onBackPressedDispatcher.addCallback(activity, callback) | |
| if (activity is AppCompatActivity) { | |
| activity.onBackPressedDispatcher.addCallback(activity, callback) | |
| } |
🤖 Prompt for AI Agents
In `@crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt` at line 46,
The current direct cast (activity as AppCompatActivity) before calling
onBackPressedDispatcher.addCallback is unsafe and can throw ClassCastException;
update AppPlugin.kt to perform a safe type check (e.g., use an
instanceof/is-check or a safe cast) against AppCompatActivity before calling
onBackPressedDispatcher.addCallback(activity, callback) and only register the
callback when the activity is an AppCompatActivity (otherwise skip registration
or log/handle gracefully) so the back-button behavior falls back to the system
when the activity is not AppCompatActivity.
| .setup(|_app, _api| { | ||
| #[cfg(target_os = "android")] | ||
| { | ||
| let handle = _api.register_android_plugin("app.tauri", "AppPlugin")?; | ||
| let handle_ref = &handle; | ||
| _app.manage(AppPlugin(*handle_ref)); | ||
| } | ||
| Ok(()) | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for PluginHandle definition
fd "\.rs$" -x rg -l "PluginHandle" {} \; | head -10Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 224
🏁 Script executed:
#!/bin/bash
# Get the structure and trait impls for PluginHandle
rg -n "struct PluginHandle|enum PluginHandle" --type rust -A 10 | head -50Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 843
🏁 Script executed:
#!/bin/bash
# Search for Copy/Clone implementations for PluginHandle
rg -n "impl.*Copy.*PluginHandle|impl.*Clone.*PluginHandle|#\[derive\(.*Copy.*\)|#\[derive\(.*Clone.*\)" --type rust -B 3 -A 5 | grep -A 5 -B 5 "PluginHandle"Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 816
🏁 Script executed:
#!/bin/bash
# Read the specific lines mentioned in the review
sed -n '135,143p' crates/tauri/src/app/plugin.rsRepository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 338
🏁 Script executed:
#!/bin/bash
# Get more context around those lines
sed -n '130,150p' crates/tauri/src/app/plugin.rsRepository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 562
🏁 Script executed:
#!/bin/bash
# Search for AppPlugin struct definition
rg -n "struct AppPlugin|impl.*AppPlugin" --type rust -A 5 | head -30Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 379
This code does not compile as written.
PluginHandle implements Clone but not Copy. The pattern let handle_ref = &handle; _app.manage(AppPlugin(*handle_ref)); violates Rust's move semantics—you cannot move a non-Copy value out of a reference. This is a compilation error that must be fixed.
Remove the intermediate reference and pass the owned handle directly:
♻️ Corrected code
.setup(|_app, _api| {
#[cfg(target_os = "android")]
{
let handle = _api.register_android_plugin("app.tauri", "AppPlugin")?;
- let handle_ref = &handle;
- _app.manage(AppPlugin(*handle_ref));
+ _app.manage(AppPlugin(handle));
}
Ok(())
})🤖 Prompt for AI Agents
In `@crates/tauri/src/app/plugin.rs` around lines 135 - 143, The setup closure is
attempting to dereference a reference to a PluginHandle which is not Copy;
remove the intermediate reference and pass the owned handle (or clone it)
directly into AppPlugin. Locate the call to register_android_plugin and the
AppPlugin construction inside the .setup closure and replace the pattern let
handle = _api.register_android_plugin(...)?; let handle_ref = &handle;
_app.manage(AppPlugin(*handle_ref)); with a direct move or clone into
_app.manage (e.g., _app.manage(AppPlugin(handle)) or
_app.manage(AppPlugin(handle.clone()))), since PluginHandle implements Clone but
not Copy.
Benchmark PR from qodo-benchmark#162
Summary by CodeRabbit
New Features
Chores
✏️ Tip: You can customize this high-level summary in your review settings.