diff --git a/src/BlazorWebView/src/Maui/Android/BlazorAndroidWebView.cs b/src/BlazorWebView/src/Maui/Android/BlazorAndroidWebView.cs
index c10c462824cb..fb98ef7ed23a 100644
--- a/src/BlazorWebView/src/Maui/Android/BlazorAndroidWebView.cs
+++ b/src/BlazorWebView/src/Maui/Android/BlazorAndroidWebView.cs
@@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Components.WebView.Maui
///
internal class BlazorAndroidWebView : AWebView
{
+ internal bool BackNavigationHandled { get; set; }
+
///
/// Initializes a new instance of
///
@@ -22,8 +24,10 @@ public override bool OnKeyDown(Keycode keyCode, KeyEvent? e)
if (keyCode == Keycode.Back && CanGoBack() && e?.RepeatCount == 0)
{
GoBack();
+ BackNavigationHandled = true;
return true;
}
+ BackNavigationHandled = false;
return false;
}
}
diff --git a/src/BlazorWebView/src/Maui/Android/BlazorWebViewHandler.Android.cs b/src/BlazorWebView/src/Maui/Android/BlazorWebViewHandler.Android.cs
index 26788b72781d..daab566211a6 100644
--- a/src/BlazorWebView/src/Maui/Android/BlazorWebViewHandler.Android.cs
+++ b/src/BlazorWebView/src/Maui/Android/BlazorWebViewHandler.Android.cs
@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
+using Android.Window;
using Android.Webkit;
using Android.Widget;
using Microsoft.Extensions.DependencyInjection;
@@ -9,6 +10,7 @@
using Microsoft.Maui;
using Microsoft.Maui.Dispatching;
using Microsoft.Maui.Handlers;
+using Microsoft.Maui.LifecycleEvents;
using static global::Android.Views.ViewGroup;
using AWebView = global::Android.Webkit.WebView;
using Path = System.IO.Path;
@@ -21,10 +23,27 @@ public partial class BlazorWebViewHandler : ViewHandler _webviewManager;
+ private AndroidLifecycle.OnBackPressed? _onBackPressedHandler;
+ BlazorWebViewPredictiveBackCallback? _predictiveBackCallback;
private ILogger? _logger;
internal ILogger Logger => _logger ??= Services!.GetService>() ?? NullLogger.Instance;
+ ///
+ /// Gets the concrete LifecycleEventService to access internal RemoveEvent method.
+ /// RemoveEvent is internal because it's not part of the public ILifecycleEventService contract,
+ /// but is needed for proper cleanup of lifecycle event handlers.
+ ///
+ private LifecycleEventService? TryGetLifecycleEventService()
+ {
+ var services = MauiContext?.Services;
+ if (services != null)
+ {
+ return services.GetService() as LifecycleEventService;
+ }
+ return null;
+ }
+
protected override AWebView CreatePlatformView()
{
Logger.CreatingAndroidWebkitWebView();
@@ -60,10 +79,89 @@ protected override AWebView CreatePlatformView()
return blazorAndroidWebView;
}
+ ///
+ /// Connects the handler to the Android and registers platform-specific
+ /// back navigation handling so that the WebView can consume back presses before the page is popped.
+ ///
+ /// The native Android instance associated with this handler.
+ ///
+ /// This override calls the base implementation and then registers an
+ /// lifecycle event handler. The handler checks and, when possible, navigates
+ /// back within the WebView instead of allowing the back press (or predictive back gesture on Android 13+)
+ /// to propagate and pop the containing page.
+ ///
+ /// When multiple BlazorWebView instances exist, the handler includes focus and visibility checks to ensure
+ /// only the currently visible and focused WebView handles the back navigation, preventing conflicts between instances.
+ ///
+ /// Inheritors that override this method should call the base implementation to preserve this back navigation
+ /// behavior unless they intentionally replace it.
+ ///
+ protected override void ConnectHandler(AWebView platformView)
+ {
+ base.ConnectHandler(platformView);
+
+ // Register OnBackPressed lifecycle event handler to check WebView's back navigation
+ // This ensures predictive back gesture (Android 13+) checks WebView.CanGoBack() before popping page
+ var lifecycleService = TryGetLifecycleEventService();
+ if (lifecycleService != null)
+ {
+ // Create a weak reference to avoid memory leaks
+ var weakPlatformView = new WeakReference(platformView);
+
+ AndroidLifecycle.OnBackPressed handler = (activity) =>
+ {
+ // Check if WebView is still alive, attached to window, and has focus
+ // This prevents non-visible or unfocused BlazorWebView instances from
+ // incorrectly intercepting back navigation when multiple instances exist
+ if (weakPlatformView.TryGetTarget(out var webView) &&
+ webView.IsAttachedToWindow &&
+ webView.HasWindowFocus &&
+ webView.CanGoBack())
+ {
+ webView.GoBack();
+ return true; // Prevent back propagation - handled by WebView
+ }
+
+ return false; // Allow back propagation - let page be popped
+ };
+
+ // Register with lifecycle service - will be invoked by HandleBackNavigation in MauiAppCompatActivity
+ lifecycleService.AddEvent(nameof(AndroidLifecycle.OnBackPressed), handler);
+ _onBackPressedHandler = handler;
+ }
+
+ if (OperatingSystem.IsAndroidVersionAtLeast(33) && _predictiveBackCallback is null)
+ {
+ if (Microsoft.Maui.ApplicationModel.Platform.CurrentActivity is not null)
+ {
+ _predictiveBackCallback = new BlazorWebViewPredictiveBackCallback(this);
+ Microsoft.Maui.ApplicationModel.Platform.CurrentActivity?.OnBackInvokedDispatcher?.RegisterOnBackInvokedCallback(0, _predictiveBackCallback);
+ }
+ }
+ }
+
private const string AndroidFireAndForgetAsyncSwitch = "BlazorWebView.AndroidFireAndForgetAsync";
protected override void DisconnectHandler(AWebView platformView)
{
+ if (OperatingSystem.IsAndroidVersionAtLeast(33) && _predictiveBackCallback is not null)
+ {
+ Microsoft.Maui.ApplicationModel.Platform.CurrentActivity?.OnBackInvokedDispatcher?.UnregisterOnBackInvokedCallback(_predictiveBackCallback);
+ _predictiveBackCallback.Dispose();
+ _predictiveBackCallback = null;
+ }
+
+ // Clean up lifecycle event handler to prevent memory leaks
+ if (_onBackPressedHandler != null)
+ {
+ var lifecycleService = TryGetLifecycleEventService();
+ if (lifecycleService != null)
+ {
+ lifecycleService.RemoveEvent(nameof(AndroidLifecycle.OnBackPressed), _onBackPressedHandler);
+ _onBackPressedHandler = null;
+ }
+ }
+
platformView.StopLoading();
if (_webviewManager != null)
@@ -182,5 +280,43 @@ public virtual async Task TryDispatchAsync(Action workIt
return await _webviewManager.TryDispatchAsync(workItem);
}
+
+ sealed class BlazorWebViewPredictiveBackCallback : Java.Lang.Object, IOnBackInvokedCallback
+ {
+ WeakReference _weakBlazorWebViewHandler;
+
+ public BlazorWebViewPredictiveBackCallback(BlazorWebViewHandler handler)
+ {
+ _weakBlazorWebViewHandler = new WeakReference(handler);
+ }
+
+ public void OnBackInvoked()
+ {
+ // KeyDown for Back button is handled in BlazorAndroidWebView.
+ // Here we just need to check if it was handled there.
+ // If not, we propagate the back press to the Activity's OnBackPressedDispatcher.
+ if (_weakBlazorWebViewHandler is not null && _weakBlazorWebViewHandler.TryGetTarget(out var handler))
+ {
+ var webView = handler.PlatformView as BlazorAndroidWebView;
+ if (webView is not null)
+ {
+ var wasBackNavigationHandled = webView.BackNavigationHandled;
+ // reset immediately for next back event
+ webView.BackNavigationHandled = false;
+
+ if (!wasBackNavigationHandled)
+ {
+ if (webView.CanGoBack()) // If we can go back in WeView, Navigate back
+ {
+ webView.GoBack();
+ return;
+ }
+ // Otherwise propagate back press to Activity
+ (Microsoft.Maui.ApplicationModel.Platform.CurrentActivity as AndroidX.AppCompat.App.AppCompatActivity)?.OnBackPressedDispatcher?.OnBackPressed();
+ }
+ }
+ }
+ }
+ }
}
}
diff --git a/src/BlazorWebView/src/Maui/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/BlazorWebView/src/Maui/PublicAPI/net-android/PublicAPI.Unshipped.txt
index 7dc5c58110bf..51e039fc88da 100644
--- a/src/BlazorWebView/src/Maui/PublicAPI/net-android/PublicAPI.Unshipped.txt
+++ b/src/BlazorWebView/src/Maui/PublicAPI/net-android/PublicAPI.Unshipped.txt
@@ -1 +1,2 @@
#nullable enable
+override Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler.ConnectHandler(Android.Webkit.WebView! platformView) -> void
diff --git a/src/BlazorWebView/tests/DeviceTests/Elements/BlazorWebViewTests.BackNavigation.cs b/src/BlazorWebView/tests/DeviceTests/Elements/BlazorWebViewTests.BackNavigation.cs
new file mode 100644
index 000000000000..227fa6d74b8b
--- /dev/null
+++ b/src/BlazorWebView/tests/DeviceTests/Elements/BlazorWebViewTests.BackNavigation.cs
@@ -0,0 +1,111 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components.WebView.Maui;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Maui.LifecycleEvents;
+using Xunit;
+
+namespace Microsoft.Maui.MauiBlazorWebView.DeviceTests.Elements;
+
+public partial class BlazorWebViewTests
+{
+#if ANDROID
+ ///
+ /// Verifies that BlazorWebViewHandler registers an OnBackPressed lifecycle event handler
+ /// when connected on Android. This handler is essential for proper back navigation within
+ /// the BlazorWebView on Android 13+ with predictive back gestures.
+ /// See: https://github.com/dotnet/maui/issues/32767
+ ///
+ [Fact]
+ public async Task BlazorWebViewRegistersOnBackPressedHandler()
+ {
+ EnsureHandlerCreated(additionalCreationActions: appBuilder =>
+ {
+ appBuilder.Services.AddMauiBlazorWebView();
+ });
+
+ var bwv = new BlazorWebViewWithCustomFiles
+ {
+ HostPage = "wwwroot/index.html",
+ CustomFiles = new Dictionary
+ {
+ { "index.html", TestStaticFilesContents.DefaultMauiIndexHtmlContent },
+ },
+ };
+ bwv.RootComponents.Add(new RootComponent { ComponentType = typeof(MauiBlazorWebView.DeviceTests.Components.NoOpComponent), Selector = "#app", });
+
+ await InvokeOnMainThreadAsync(async () =>
+ {
+ var bwvHandler = CreateHandler(bwv);
+ var platformWebView = bwvHandler.PlatformView;
+ await WebViewHelpers.WaitForWebViewReady(platformWebView);
+
+ // Get the lifecycle event service and verify OnBackPressed handler is registered
+ var lifecycleService = MauiContext.Services.GetService() as LifecycleEventService;
+ Assert.NotNull(lifecycleService);
+
+ // Verify the OnBackPressed event has been registered
+ Assert.True(lifecycleService.ContainsEvent(nameof(AndroidLifecycle.OnBackPressed)),
+ "BlazorWebViewHandler should register an OnBackPressed lifecycle event handler on Android");
+ });
+ }
+
+ ///
+ /// Verifies that BlazorWebViewHandler properly cleans up the OnBackPressed lifecycle event handler
+ /// when disconnected. This prevents memory leaks and ensures proper cleanup.
+ /// See: https://github.com/dotnet/maui/issues/32767
+ ///
+ [Fact]
+ public async Task BlazorWebViewCleansUpOnBackPressedHandlerOnDisconnect()
+ {
+ EnsureHandlerCreated(additionalCreationActions: appBuilder =>
+ {
+ appBuilder.Services.AddMauiBlazorWebView();
+ });
+
+ var bwv = new BlazorWebViewWithCustomFiles
+ {
+ HostPage = "wwwroot/index.html",
+ CustomFiles = new Dictionary
+ {
+ { "index.html", TestStaticFilesContents.DefaultMauiIndexHtmlContent },
+ },
+ };
+ bwv.RootComponents.Add(new RootComponent { ComponentType = typeof(MauiBlazorWebView.DeviceTests.Components.NoOpComponent), Selector = "#app", });
+
+ await InvokeOnMainThreadAsync(async () =>
+ {
+ var bwvHandler = CreateHandler(bwv);
+ var platformWebView = bwvHandler.PlatformView;
+ await WebViewHelpers.WaitForWebViewReady(platformWebView);
+
+ var lifecycleService = MauiContext.Services.GetService() as LifecycleEventService;
+ Assert.NotNull(lifecycleService);
+
+ // Verify handler is registered after connect
+ Assert.True(lifecycleService.ContainsEvent(nameof(AndroidLifecycle.OnBackPressed)),
+ "OnBackPressed handler should be registered after ConnectHandler");
+
+ // Count the handlers before disconnect
+ var handlersBefore = lifecycleService.GetEventDelegates(nameof(AndroidLifecycle.OnBackPressed));
+ int countBefore = 0;
+ foreach (var _ in handlersBefore)
+ countBefore++;
+
+ // Disconnect the handler by setting the BlazorWebView's Handler to null
+ // This triggers DisconnectHandler internally
+ bwv.Handler = null;
+
+ // Count the handlers after disconnect
+ var handlersAfter = lifecycleService.GetEventDelegates(nameof(AndroidLifecycle.OnBackPressed));
+ int countAfter = 0;
+ foreach (var _ in handlersAfter)
+ countAfter++;
+
+ // Verify the handler count decreased (cleanup happened)
+ Assert.True(countAfter < countBefore,
+ $"OnBackPressed handler should be removed after DisconnectHandler. Before: {countBefore}, After: {countAfter}");
+ });
+ }
+#endif
+}
diff --git a/src/Core/src/LifecycleEvents/LifecycleEventService.cs b/src/Core/src/LifecycleEvents/LifecycleEventService.cs
index e861166f2798..4a35b494987a 100644
--- a/src/Core/src/LifecycleEvents/LifecycleEventService.cs
+++ b/src/Core/src/LifecycleEvents/LifecycleEventService.cs
@@ -39,5 +39,15 @@ public IEnumerable GetEventDelegates(string eventName)
public bool ContainsEvent(string eventName) =>
_mapper.TryGetValue(eventName, out var delegates) && delegates?.Count > 0;
+
+ internal void RemoveEvent(string eventName, TDelegate action)
+ where TDelegate : Delegate
+ {
+ if (_mapper.TryGetValue(eventName, out var delegates) && delegates != null)
+ {
+ if (delegates.Remove(action) && delegates.Count == 0)
+ _mapper.Remove(eventName);
+ }
+ }
}
}
\ No newline at end of file