feat(core): back button event on Android, closes #8142 - #13
Conversation
…8142 I've used https://github.com/ionic-team/capacitor-plugins/blob/main/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java as a reference here, checking if there's a back button event handler with a default of webview's goBack implementation
WalkthroughThis PR introduces Android back-button press event handling across the Tauri framework. Changes include a new Android plugin that intercepts back presses, registers listener callbacks, and emits events to the JavaScript layer. Companion updates add Rust-side plugin registration, TypeScript API bindings, permission declarations, and dependency configuration. Changes
Sequence DiagramsequenceDiagram
participant User as User
participant Activity as TauriActivity
participant AppPlugin as AppPlugin
participant WebView as WebView
participant Rust as Rust Runtime
participant JS as JavaScript
User->>Activity: Press back button
Activity->>AppPlugin: OnBackPressedCallback triggered
AppPlugin->>AppPlugin: Check hasListener("back-button")?
alt Listener registered
AppPlugin->>WebView: canGoBack()?
alt WebView can go back
WebView->>WebView: Navigate back
else WebView cannot go back
AppPlugin->>Activity: Temporarily disable callback
Activity->>Activity: Call onBackPressed()
AppPlugin->>Activity: Re-enable callback
end
else No listener registered
AppPlugin->>WebView: Get canGoBack state
AppPlugin->>Rust: Emit BACK_BUTTON_EVENT
Rust->>JS: Dispatch onBackButtonPress event
JS->>JS: Handler receives {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 direct cast (activity as AppCompatActivity) in AppPlugin.kt can
throw ClassCastException if the host is not an AppCompatActivity; change this to
a safe check (e.g., use a safe cast or instanceof check) before calling
onBackPressedDispatcher.addCallback and handle the non-AppCompatActivity case
(log a warning, fall back to activity.onBackPressed()/no-op, or document the
requirement). Update the code that references activity and
onBackPressedDispatcher.addCallback and ensure callback is registered only when
the activity is an AppCompatActivity to avoid runtime crashes.
- Line 35: Replace the deprecated call to activity.onBackPressed() inside
AppPlugin with the OnBackPressedDispatcher API: invoke
activity.onBackPressedDispatcher.onBackPressed() (or dispatch through the
activity's OnBackPressedDispatcher) so the plugin delegates back handling via
the modern dispatcher; if you need the temporary self-disable/delegate pattern
keep the existing callback suspension logic but call the dispatcher instead, or
alternatively implement a simple chain-of-responsibility of callbacks in
AppPlugin to forward the back event to the next handler when appropriate.
- Around line 27-47: The back-button handling is inverted and uses an unsafe
cast; update the OnBackPressedCallback in AppPlugin so that when
hasListener(BACK_BUTTON_EVENT) is true you call trigger(BACK_BUTTON_EVENT, data)
to emit the event, and only when there are no listeners perform default
navigation (if webView?.canGoBack() == true call webView.goBack() else invoke
the platform back action). Replace the unsafe (activity as
AppCompatActivity).onBackPressedDispatcher.addCallback(...) with a safe check
for activity being a ComponentActivity (or AppCompatActivity): if activity is
ComponentActivity use its onBackPressedDispatcher to addCallback(activity,
callback); otherwise fall back to calling activity.onBackPressed() for the
default back action. Ensure you reference BACK_BUTTON_EVENT, hasListener,
trigger, webView, AppPlugin, OnBackPressedCallback, and onBackPressedDispatcher
when making these changes.
🧹 Nitpick comments (1)
packages/api/src/app.ts (1)
255-275: Enhance JSDoc with platform info, version tag, and example.The implementation is correct and follows existing patterns. However, the JSDoc could be improved for consistency with other functions in this file:
- Add
@sincetag with the appropriate version- Add
@exampleblock showing usage- Document the Android-only platform constraint (similar to
setTheme's platform note)📝 Suggested JSDoc enhancement
/** * Listens to the backButton event on Android. - * `@param` handler + * + * `@example` + * ```typescript + * import { onBackButtonPress } from '@tauri-apps/api/app'; + * const unlisten = await onBackButtonPress((payload) => { + * console.log('Can go back:', payload.canGoBack); + * }); + * ``` + * + * #### Platform-specific + * + * - **Desktop / iOS:** Unsupported. + * + * `@param` handler - The callback to invoke when the back button is pressed. + * `@returns` A promise resolving to a function to remove the listener. + * + * `@since` 2.x.0 */
📜 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 (1)
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 (16)
crates/tauri-runtime-wry/Cargo.toml (1)
20-25:wry 0.53.4feature compatibility confirmed.The
linux-bodyfeature is valid inwry 0.53.4and enables custom-protocol request body reading on Linux (requires WebKit2GTK v2.40+). No breaking API changes exist between 0.53.2 and 0.53.4. The update is compatible withtauri-runtime-wry.crates/tauri/scripts/bundle.global.js (1)
1-1: LGTM - Generated bundle includes the new back-button API.This minified bundle is auto-generated. The new
onBackButtonPressfunction is correctly wired to useaddPluginListenerwith the"back-button"event, which aligns with the Android plugin implementation.crates/tauri/src/path/android.rs (1)
7-7: LGTM - Unused import removed.The
std::ffi::OsStrimport was correctly removed as it's not used in this file. The remaining imports (Path,PathBuf) are all actively used.crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt (2)
149-151: LGTM - Clean implementation of listener presence check.The
hasListenermethod correctly uses Kotlin'sisNullOrEmpty()idiom to safely check both map entry existence and list emptiness. This enables the AppPlugin to conditionally handle back-button behavior based on listener registration.
177-181: LGTM - Good cleanup of empty listener lists.Removing the event key when its listener list becomes empty prevents unbounded growth of the map and aligns with good memory management practices.
.changes/back-button-press-api.md (1)
1-5: LGTM - Changelog entry is accurate.The entry correctly categorizes this as a minor feature addition and clearly describes the Android-specific back button handling capability.
.changes/android-app-plugin.md (1)
1-5: LGTM - Changelog entry accurately describes the feature.The entry correctly documents the mobile app plugin addition with its exit and back button press event capabilities.
crates/tauri/mobile/android-codegen/TauriActivity.kt (1)
15-15: LGTM - Enables back navigation handling by default.This override enables the back-button handling infrastructure introduced in this PR. The default behavior (webview
goBack()or app exit) aligns with standard Android user expectations.crates/tauri/src/app/plugin.rs (2)
135-142: LGTM!The Android-specific plugin registration is well-structured. Using underscore-prefixed parameters (
_app,_api) avoids unused variable warnings on non-Android targets while keeping the setup closure available for future platform-specific extensions.
146-147: LGTM!The
AppPlugin<R>struct appropriately wrapsPluginHandle<R>and is conditionally compiled only for Android. Thepub(crate)visibility is appropriate for internal use.crates/tauri/permissions/app/autogenerated/reference.md (2)
12-13: LGTM!The default permissions correctly include the new
allow-register-listenerandallow-remove-listenerentries, aligning with thebuild.rsconfiguration where both are enabled by default.
209-282: LGTM!The permission table entries for
register-listenerandremove-listener(both allow and deny variants) follow the established documentation pattern and are consistent with other permission entries.crates/tauri/build.rs (1)
167-168: LGTM!The new
register_listenerandremove_listenercommands are correctly added to thecore:appplugin with default-enabled status. This aligns with the listener-based back-button API and permission definitions.packages/api/src/app.ts (2)
5-5: LGTM!The import statement correctly brings in
addPluginListenerandPluginListenerfrom the core module for the new listener API.
289-291: LGTM!The exports are correctly added for the new type and function.
crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt (1)
49-53: LGTM!The
exitcommand correctly resolves the invoke and finishes the activity. The order (resolve then finish) ensures the JS side receives acknowledgment before the activity terminates.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| init { | ||
| val callback = object : OnBackPressedCallback(true) { | ||
| override fun handleOnBackPressed() { | ||
| if (hasListener(BACK_BUTTON_EVENT)) { | ||
| if (this@AppPlugin.webView?.canGoBack() == true) { | ||
| this@AppPlugin.webView!!.goBack() | ||
| } else { | ||
| this.isEnabled = false | ||
| this@AppPlugin.activity.onBackPressed() | ||
| this.isEnabled = true | ||
| } | ||
| } else { | ||
| val data = JSObject().apply { | ||
| put("canGoBack", this@AppPlugin.webView?.canGoBack() ?: false) | ||
| } | ||
| trigger(BACK_BUTTON_EVENT, data) | ||
| } | ||
| } | ||
| } | ||
| (activity as AppCompatActivity).onBackPressedDispatcher.addCallback(activity, callback) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, let's understand the context by examining the AppPlugin.kt file more thoroughly
cat -n crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt | head -60Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 2001
🏁 Script executed:
#!/bin/bash
# Search for hasListener method definition and its usage pattern
rg -n "hasListener" --type=kotlin -B2 -A2 crates/tauri/Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 1143
🏁 Script executed:
#!/bin/bash
# Look for BACK_BUTTON_EVENT and how it's used elsewhere
rg -n "BACK_BUTTON_EVENT" --type=kotlin -B3 -A3 crates/tauri/Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 2164
🏁 Script executed:
#!/bin/bash
# Check for any tests or documentation about back button behavior
rg -n -i "back.*button|back.*press" --type=md,java,kotlin crates/tauri/ | head -40Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 122
🏁 Script executed:
#!/bin/bash
# Search for activity.onBackPressed() usage to see if it's deprecated
rg -n "onBackPressed\(\)" --type=kotlin crates/tauri/ -B2 -A2Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 575
🏁 Script executed:
#!/bin/bash
# Check Android API level targets and deprecation notes
rg -n "compileSdk\|targetSdk\|OnBackPressedCallback" crates/tauri/mobile/android/ | head -20Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 66
🏁 Script executed:
#!/bin/bash
# Search for any related test cases or documentation
rg -n "back-button" --type=kotlin crates/tauri/Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 199
🏁 Script executed:
#!/bin/bash
# Look for build.gradle or manifest to check target SDK
find crates/tauri/mobile/android -name "build.gradle*" -o -name "AndroidManifest.xml" | xargs cat 2>/dev/null | grep -i "targetSdk\|compileSdk" | head -10Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 103
🏁 Script executed:
#!/bin/bash
# Search for Plugin base class to understand trigger() and hasListener() contract
cat -n crates/tauri/mobile/android/src/main/java/app/tauri/plugin/Plugin.kt | head -200Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 7524
🏁 Script executed:
#!/bin/bash
# Look for trigger() method definition to understand its semantics
rg -n "fun trigger" --type=kotlin crates/tauri/mobile/android/ -A3Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 1059
Critical: Back button handling logic is inverted.
When hasListener(BACK_BUTTON_EVENT) returns true (listeners are registered), the code performs default navigation (goBack/onBackPressed) instead of emitting the event. When no listeners exist, it calls trigger() which silently does nothing.
This is backwards—emit the event to registered listeners so they can handle it; only perform default behavior when no listeners are registered.
Also flag:
- Line 35:
activity.onBackPressed()is deprecated in API 33+ (compileSdk=36); useonBackPressedDispatcherinstead. - Line 46: Unsafe cast
(activity as AppCompatActivity)without null check; Plugin acceptsActivity, notAppCompatActivity, risking runtime crash.
🤖 Prompt for AI Agents
In `@crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt` around
lines 27 - 47, The back-button handling is inverted and uses an unsafe cast;
update the OnBackPressedCallback in AppPlugin so that when
hasListener(BACK_BUTTON_EVENT) is true you call trigger(BACK_BUTTON_EVENT, data)
to emit the event, and only when there are no listeners perform default
navigation (if webView?.canGoBack() == true call webView.goBack() else invoke
the platform back action). Replace the unsafe (activity as
AppCompatActivity).onBackPressedDispatcher.addCallback(...) with a safe check
for activity being a ComponentActivity (or AppCompatActivity): if activity is
ComponentActivity use its onBackPressedDispatcher to addCallback(activity,
callback); otherwise fall back to calling activity.onBackPressed() for the
default back action. Ensure you reference BACK_BUTTON_EVENT, hasListener,
trigger, webView, AppPlugin, OnBackPressedCallback, and onBackPressedDispatcher
when making these changes.
| this@AppPlugin.webView!!.goBack() | ||
| } else { | ||
| this.isEnabled = false | ||
| this@AppPlugin.activity.onBackPressed() |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Android onBackPressed deprecated API 33 OnBackPressedDispatcher
💡 Result:
Short answer
- Activity.onBackPressed() is deprecated starting API 33 (Android 13). Use OnBackPressedDispatcher with an OnBackPressedCallback (the AndroidX dispatcher adapts to the platform OnBackInvoked APIs for predictive back). [1][2]
Kotlin example
- Simple lifecycle-aware callback:
onBackPressedDispatcher.addCallback(this) {
// handle back
finish()
}
Java example - Using OnBackPressedCallback:
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Overridepublic void handleOnBackPressed() { finish(); }
});
Notes
- If you target API 33+ the platform introduces OnBackInvokedCallback; using androidx.activity’s OnBackPressedDispatcher (Activity 1.6+/androidx.activity) ensures compatibility and predictive-back behavior. [1][3]
References
[1] AndroidX OnBackPressedDispatcher API reference.
[2] Android Developers — Provide custom back navigation (migration guidance).
[3] StackOverflow — recommended usage / examples for OnBackPressedDispatcher.
🏁 Script executed:
cat -n crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt | head -60Repository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 2001
🏁 Script executed:
rg "OnBackPressedDispatcher" --type javaRepository: qodo-benchmark/tauri-combined-coderabbit
Length of output: 250
activity.onBackPressed() is deprecated in Android API 33+.
The onBackPressed() method is deprecated as of Android 13. The code uses this deprecated method on line 35 within a callback that temporarily disables itself to delegate to the system's default back behavior. While this pattern works, it relies on a deprecated API. Consider implementing a chain of responsibility pattern with multiple callbacks or documenting this limitation for forward compatibility with Android 14+.
🤖 Prompt for AI Agents
In `@crates/tauri/mobile/android/src/main/java/app/tauri/AppPlugin.kt` at line 35,
Replace the deprecated call to activity.onBackPressed() inside AppPlugin with
the OnBackPressedDispatcher API: invoke
activity.onBackPressedDispatcher.onBackPressed() (or dispatch through the
activity's OnBackPressedDispatcher) so the plugin delegates back handling via
the modern dispatcher; if you need the temporary self-disable/delegate pattern
keep the existing callback suspension logic but call the dispatcher instead, or
alternatively implement a simple chain-of-responsibility of callbacks in
AppPlugin to forward the back event to the next handler when appropriate.
| } | ||
| } | ||
| } | ||
| (activity as AppCompatActivity).onBackPressedDispatcher.addCallback(activity, callback) |
There was a problem hiding this comment.
Unsafe cast to AppCompatActivity without validation.
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)
+ }📝 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 direct cast (activity as AppCompatActivity) in AppPlugin.kt can throw
ClassCastException if the host is not an AppCompatActivity; change this to a
safe check (e.g., use a safe cast or instanceof check) before calling
onBackPressedDispatcher.addCallback and handle the non-AppCompatActivity case
(log a warning, fall back to activity.onBackPressed()/no-op, or document the
requirement). Update the code that references activity and
onBackPressedDispatcher.addCallback and ensure callback is registered only when
the activity is an AppCompatActivity to avoid runtime crashes.
Benchmark PR from qodo-benchmark#30
Summary by CodeRabbit
New Features
Chores
✏️ Tip: You can customize this high-level summary in your review settings.