[Android] Fix for Android 16 Back button is not working after command from FlyoutPage#35196
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35196Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35196" |
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 6 findings
See inline comments for details.
a544a01 to
7721955
Compare
MauiBot
left a comment
There was a problem hiding this comment.
🤖 Automated review — alternative fix proposed
The expert-reviewer evaluation compared the PR fix against #1 automatically generated candidates and selected try-fix-1 as the strongest fix.
Why: try-fix-1 implements the same DrawerLayout callback release as the PR but entirely within FlyoutViewHandler.DisconnectHandler — eliminating the cross-layer Window→FlyoutPage→Handler call chain, widening the API guard from 36 to 33, and handling nested FlyoutPage topologies. It requires changes to only one file (FlyoutViewHandler.Android.cs) vs the PR's three files.
Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.
Candidate diff (`try-fix-1`)
diff --git a/src/Core/src/Handlers/FlyoutView/FlyoutViewHandler.Android.cs b/src/Core/src/Handlers/FlyoutView/FlyoutViewHandler.Android.cs
index fafb0d901c..2e408fd79b 100644
--- a/src/Core/src/Handlers/FlyoutView/FlyoutViewHandler.Android.cs
+++ b/src/Core/src/Handlers/FlyoutView/FlyoutViewHandler.Android.cs
@@ -311,6 +311,22 @@ namespace Microsoft.Maui.Handlers
protected override void DisconnectHandler(View platformView)
{
+ _pendingFragment?.Dispose();
+ _pendingFragment = null;
+
+ // On Android 13+ (API 33+), DrawerLayout registers a system OnBackInvokedCallback
+ // when it has an open or openable drawer. If the handler disconnects without closing
+ // the drawer and locking it first, that callback survives and shadows any new page's
+ // back-handling callbacks. Close and lock the drawer synchronously so the system
+ // callback is unregistered before the view hierarchy is torn down.
+ if (OperatingSystem.IsAndroidVersionAtLeast(33) && platformView is DrawerLayout drawerLayout)
+ {
+ if (_flyoutView is not null && _flyoutView.Parent == drawerLayout)
+ drawerLayout.CloseDrawer(_flyoutView, false);
+
+ drawerLayout.SetDrawerLockMode(DrawerLayout.LockModeLockedClosed);
+ }
+
MauiWindowInsetListener.UnregisterView(platformView);
if (_navigationRoot is CoordinatorLayout cl)
{
kubaflo
left a comment
There was a problem hiding this comment.
Could you please try ai's suggestions?
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 5 findings
See inline comments for details.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 7 findings
See inline comments for details.
…View2 is not connected in Appium. (dotnet#35335) ### Description of Changes - Recently, the Appium driver has not been connecting properly to the native WebView2 control on Windows. While running locally using Appium Inspector with the WebView control, the inspector is unable to recognize the WebView and displays an error. - Due to this Appium driver issue, the WebView lane in CI takes a long time to run (approximately 3 hours) and eventually gets cancelled. As a temporary workaround, the WebView lane has been temporarily removed from the Windows CI pipeline to allow the CI process to complete more quickly. <img width="649" height="294" alt="image" src="https://github.com/user-attachments/assets/68df006b-56d6-4bfa-870a-a4184f5b18b7" /> <img width="576" height="430" alt="image" src="https://github.com/user-attachments/assets/40c222e8-4935-450d-be7e-5ee9245e9eb1" /> **Issue:** dotnet#35334
…ler state on Android 16 (API 36) when replacing Window.Page from an open FlyoutPage, preventing stale back-interception behavior.
fe3f138 to
c2eeac7
Compare
|
/azp run maui-pr-uitests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
🤖 AI Summary
📊 Review Session —
|
| Category | Tests | Snapshot diffs |
|---|---|---|
controls-FlyoutPage |
10/14 (4 ❌) | — |
controls-ViewBaseTests |
128/133 (4 ❌) | — |
controls-Window |
128/133 (4 ❌) | — |
❌ controls-FlyoutPage — 4 failed tests
VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover
Flyout did not open after multiple drag attempts
Assert.That(flyoutOpened, Is.True)
Expected: True
But was: False
at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 149
1) at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 149
ShouldKeepFlyoutLockedWhenSwitchingLandScapeToPortrait
OpenQA.Selenium.InvalidElementStateException : Screen rotation cannot be changed to ROTATION_0 after 2000ms. Is it locked programmatically?
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.set_Orientation(ScreenOrientation value)
at UITest.Appium.AppiumOrientationActions.SetOrientationPortrait(IDictionary`2 parameters) in /_/src/TestUtils/src/UITest.Appium/Actions/AppiumOrientationActions.cs:line 43
at UITest.Appium.AppiumOrientationActions.Execute(String commandName, IDictionary`2 parameters) in /_/src/TestUtils/src/UITest.Appium/Actions/AppiumOrientationActions.cs:line 34
at UITest.Appium.AppiumCommandExecutor.Execute(String commandName, IDictionary`2 parameters) in /_/src/TestUti
...
VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover
Flyout did not open after multiple drag attempts
Assert.That(flyoutOpened, Is.True)
Expected: True
But was: False
at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 149
1) at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 149
ShouldKeepFlyoutLockedWhenSwitchingLandScapeToPortrait
OpenQA.Selenium.InvalidElementStateException : Screen rotation cannot be changed to ROTATION_0 after 2000ms. Is it locked programmatically?
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.set_Orientation(ScreenOrientation value)
at UITest.Appium.AppiumOrientationActions.SetOrientationPortrait(IDictionary`2 parameters) in /_/src/TestUtils/src/UITest.Appium/Actions/AppiumOrientationActions.cs:line 43
at UITest.Appium.AppiumOrientationActions.Execute(String commandName, IDictionary`2 parameters) in /_/src/TestUtils/src/UITest.Appium/Actions/AppiumOrientationActions.cs:line 34
at UITest.Appium.AppiumCommandExecutor.Execute(String commandName, IDictionary`2 parameters) in /_/src/TestUti
...
❌ controls-ViewBaseTests — 4 failed tests
VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover
Flyout did not open after multiple drag attempts
Assert.That(flyoutOpened, Is.True)
Expected: True
But was: False
at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 149
1) at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 149
ShouldKeepFlyoutLockedWhenSwitchingLandScapeToPortrait
OpenQA.Selenium.InvalidElementStateException : Screen rotation cannot be changed to ROTATION_0 after 2000ms. Is it locked programmatically?
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.set_Orientation(ScreenOrientation value)
at UITest.Appium.AppiumOrientationActions.SetOrientationPortrait(IDictionary`2 parameters) in /_/src/TestUtils/src/UITest.Appium/Actions/AppiumOrientationActions.cs:line 43
at UITest.Appium.AppiumOrientationActions.Execute(String commandName, IDictionary`2 parameters) in /_/src/TestUtils/src/UITest.Appium/Actions/AppiumOrientationActions.cs:line 34
at UITest.Appium.AppiumCommandExecutor.Execute(String commandName, IDictionary`2 parameters) in /_/src/TestUti
...
VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover
Flyout did not open after multiple drag attempts
Assert.That(flyoutOpened, Is.True)
Expected: True
But was: False
at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 149
1) at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 149
ShouldKeepFlyoutLockedWhenSwitchingLandScapeToPortrait
OpenQA.Selenium.InvalidElementStateException : Screen rotation cannot be changed to ROTATION_0 after 2000ms. Is it locked programmatically?
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.set_Orientation(ScreenOrientation value)
at UITest.Appium.AppiumOrientationActions.SetOrientationPortrait(IDictionary`2 parameters) in /_/src/TestUtils/src/UITest.Appium/Actions/AppiumOrientationActions.cs:line 43
at UITest.Appium.AppiumOrientationActions.Execute(String commandName, IDictionary`2 parameters) in /_/src/TestUtils/src/UITest.Appium/Actions/AppiumOrientationActions.cs:line 34
at UITest.Appium.AppiumCommandExecutor.Execute(String commandName, IDictionary`2 parameters) in /_/src/TestUti
...
❌ controls-Window — 4 failed tests
VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover
Flyout did not open after multiple drag attempts
Assert.That(flyoutOpened, Is.True)
Expected: True
But was: False
at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 149
1) at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 149
ShouldKeepFlyoutLockedWhenSwitchingLandScapeToPortrait
OpenQA.Selenium.InvalidElementStateException : Screen rotation cannot be changed to ROTATION_0 after 2000ms. Is it locked programmatically?
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.set_Orientation(ScreenOrientation value)
at UITest.Appium.AppiumOrientationActions.SetOrientationPortrait(IDictionary`2 parameters) in /_/src/TestUtils/src/UITest.Appium/Actions/AppiumOrientationActions.cs:line 43
at UITest.Appium.AppiumOrientationActions.Execute(String commandName, IDictionary`2 parameters) in /_/src/TestUtils/src/UITest.Appium/Actions/AppiumOrientationActions.cs:line 34
at UITest.Appium.AppiumCommandExecutor.Execute(String commandName, IDictionary`2 parameters) in /_/src/TestUti
...
VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover
Flyout did not open after multiple drag attempts
Assert.That(flyoutOpened, Is.True)
Expected: True
But was: False
at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 149
1) at Microsoft.Maui.TestCases.Tests.FlyoutPageFeatureTests.VerifyFlyoutPage_IsGestureEnabled_FlyoutBehaviorPopover() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/FlyoutPageFeatureTests.cs:line 149
ShouldKeepFlyoutLockedWhenSwitchingLandScapeToPortrait
OpenQA.Selenium.InvalidElementStateException : Screen rotation cannot be changed to ROTATION_0 after 2000ms. Is it locked programmatically?
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.Appium.AppiumDriver.set_Orientation(ScreenOrientation value)
at UITest.Appium.AppiumOrientationActions.SetOrientationPortrait(IDictionary`2 parameters) in /_/src/TestUtils/src/UITest.Appium/Actions/AppiumOrientationActions.cs:line 43
at UITest.Appium.AppiumOrientationActions.Execute(String commandName, IDictionary`2 parameters) in /_/src/TestUtils/src/UITest.Appium/Actions/AppiumOrientationActions.cs:line 34
at UITest.Appium.AppiumCommandExecutor.Execute(String commandName, IDictionary`2 parameters) in /_/src/TestUti
...
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)
🔍 Pre-Flight — Context & Validation
Pre-Flight: PR #35196
Issue
- Android: BackButton on Android 16 not working after command from FlyOutPage #33508 — On Android 16 (API 36), after navigating from an open
FlyoutPage(via flyout button that replacesWindow.Page), the system back button no longer reachesPage.OnBackButtonPressed()on the new page. - Repro requires real device on API 36; works on 13/14/15.
- Root cause: Android 16 made predictive back mandatory.
DrawerLayoutnow registers its own callback directly withOnBackInvokedDispatcher, bypassing MAUI'sOnBackPressedDispatcherinterception. WhenWindow.Pageis swapped while the drawer's callback is still registered, the staleDrawerLayoutcallback shadows the new page's back handlers.
PR Approach (head: 5ec60d7d)
Three-file change:
FlyoutViewHandler.Android.cs— Newinternal void ReleaseDrawerCallbackBeforePageChange()that, on API ≥36, disposes_pendingFragment,CloseDrawer(_flyoutView, false)(animated:false → synchronous), andSetDrawerLockMode(LockModeLockedClosed). A_releasingflag suppresses the spuriousIsPresentedwrite inOnDrawerStateChanged. A new helperCancelPendingFragment()deduplicates dispose pattern.DisconnectHandleralso callsCancelPendingFragment()first.Window.cs—OnPageChangingnow (Android-only) walks the outgoing page tree viaReleaseFlyoutDrawerCallbacks(oldPage), recursing throughIPageContainer<Page>.CurrentPage, castingoldPage.Handlerto concreteFlyoutViewHandler, and calling the release method beforeSendDisappearing().FlyoutPage.cs— Cosmetic blank-line addition only (the originally-proposed Android-only shim was removed in response to layering review).
Critical Constraints
- Cleanup must be synchronous and BEFORE the new page registers its back callback.
- Author confirmed in comments: any approach that relies on
DisconnectHandler()is too late, because MAUI defers handler disconnect toOnUnloaded(async, after new page connects). This rules out the prior try-fix-1 (handler-self-cleanup inDisconnectHandler).
- Author confirmed in comments: any approach that relies on
- Cleanup must run for nested cases:
NavigationPage(FlyoutPage),Shell(FlyoutPage),TabbedPage(FlyoutPage), etc. The recursive walk addresses one branch (CurrentPage) but does not cover sibling tabs or modal pages. - No CI test possible: API 36 not available on Helix; the team accepts no automated regression test here. Manual repro validated by author.
Prior Exploration (per PR's AI summary, history-trained agent)
| # | Approach | Status |
|---|---|---|
| 1 | DisconnectHandler self-cleanup with event unsubscribe | Rejected — disconnect is async, runs too late |
| 2 | FlyoutPage.OnDisappearing/OnAppearing + temp event unsub |
Passed CI but does it run synchronously before new page connects? |
| 3 | OnWindowChanged driving IsPresented=false via mapper |
Same timing concern |
| 4 | ViewDetachedFromWindow one-shot self-unsubscribing handler |
Same timing concern |
| 5 | Attachment-gated guard in OnDrawerStateChanged |
Workaround at invocation, not a release |
| 6 | Direct UnregisterOnBackInvokedCallback on dispatcher |
Blocked — DrawerLayout's internal callback not publicly accessible |
The author's last comment (post-iteration) makes clear: the only timing window that works is Window.OnPageChanging (synchronous, before SendDisappearing kicks off teardown). So genuinely new approaches must either:
- Use a different release mechanism invoked from the same
OnPageChangingwindow, OR - Use a higher-priority Activity-level callback that can win even if DrawerLayout's stale callback persists, OR
- Rework so the DrawerLayout never registers its system callback in the first place (subclass / opt-out).
Code Review Status (latest from history-trained AI bot)
- 0 errors, 2 warnings (test absence + IsPresented race — both addressed in latest iteration via
_releasingflag and TODO). - 2 suggestions (concrete cast, nested coverage — both addressed: cast is intra-handler, nested via recursive walk).
Files Touched
src/Controls/src/Core/FlyoutPage/FlyoutPage.cs(+1 LoC, blank line)src/Controls/src/Core/Window/Window.cs(+30 LoC)src/Core/src/Handlers/FlyoutView/FlyoutViewHandler.Android.cs(+67 LoC, -1 LoC)
Test Strategy for Candidates
- Build verification only:
dotnet build src/Core/src/Core.csproj -f net10.0-androidanddotnet build src/Controls/src/Core/Controls.Core.csproj -f net10.0-android. CI device tests will not catch this regression (API 30 only). Expert reviewer must judge correctness via code review.
📋 Report — Final Recommendation
Comparative Report — PR #35196
Issue: Android 16 (API 36) — FlyoutPage.OnBackButtonPressed() not invoked after Window.Page is replaced from an open FlyoutPage.
Root cause: AndroidX DrawerLayout 1.2.0+ registers an OnBackInvokedCallback (PRIORITY_OVERLAY) directly with the system OnBackInvokedDispatcher. On API 36 predictive back is mandatory, so MAUI's OnBackPressedDispatcher interception is bypassed; a stale callback shadows the new page's handlers if the drawer is still "visible" at swap time.
Gate: SKIPPED (no tests in PR, accepted — Helix is API 30 only; API 36 emulator unavailable in CI).
Candidates
| ID | Description | Build | Regression test | Coverage scope |
|---|---|---|---|---|
pr |
The PR as submitted (3 files, +98/-1) | Implicit ✓ (CI passing) | n/a (no test) — manually verified by author | NavigationPage/Shell CurrentPage only |
pr-plus-reviewer |
pr + reviewer fix: Window.cs walks all MultiPage<Page>.Children (TabbedPage non-current tabs) |
✓ Controls.Core net10.0-android36.0 | n/a (no test) | + non-current TabbedPage tabs |
try-fix-3 |
Subclass DrawerLayout as MauiDrawerLayout for "ownership" of back-callback lifecycle |
Not applied (no diff produced) | n/a | n/a |
Per-candidate evaluation
pr — PR as submitted
- Strengths: Correct release mechanism (
SetDrawerLockMode(LockModeLockedClosed)synchronously deregisters theOnBackInvokedCallback). Correct timing (OnPageChangingbeforeSendDisappearing)._releasingre-entrancy guard is sound.internalkeeps the contract out of public API. SymmetricCancelPendingFragment()extraction also closes a small leak inDisconnectHandler. - Weaknesses:
- Recursive walk follows only
IPageContainer<Page>.CurrentPage. Misses non-currentTabbedPagetabs that may host a FlyoutPage with an open drawer. - API gate
>=36doesn't catch the API 33–35 opt-in case (enableOnBackInvokedCallback="true"). Real-world but rare. - Modal stack (
Window.Navigation.ModalStack) not walked. Edge case. - Lock-mode override persists if the same FlyoutPage instance is reused without
DisconnectHandlerfiring in between. Edge case.
- Recursive walk follows only
- Test status: None. Acceptable per CI constraint.
pr-plus-reviewer — PR + reviewer feedback
- All of
pr's strengths. Plus: - Closes the non-current TabbedPage children coverage gap with a
MultiPage<Page>enumeration ofChildren, falling back toCurrentPageforNavigationPage/Shell. +12 LoC. - Build verified:
dotnet build src/Controls/src/Core/Controls.Core.csproj -f net10.0-android36.0→ 0 warnings, 0 errors. - Reviewer also flagged 3 nits + 2 suggestions left as comments for the author (API gate widening, lifecycle reuse comment, threading contract comment, side-effect comment, layering note) — none required to ship.
- Test status: None. Acceptable per CI constraint.
try-fix-3 — Subclass DrawerLayout
- Verdict in the try-fix report: "Decline this approach. Use the PR (try-fix Update README.md #2) instead."
- The investigation establishes (with AndroidX source citations) that AndroidX exposes no protected hook that a subclass could exploit —
mBackInvokedCallback,mBackInvokedDispatcher,updateBackInvokedCallbackStateare allprivate/package-private. Subclassing reduces to calling the samesetDrawerLockMode/closeDrawersthe PR already calls, wrapped in extra ceremony (Java[Register]binding, axml class-name change, R8/trim hints). - Diff: Empty — explicitly not applied.
- Build / test: None.
- Net effect: Same release mechanism as the PR, more surface area, no behavioural improvement. Worth recording as an explored-and-blocked finding, not as a viable competing fix.
Comparison matrix
| Dimension | pr |
pr-plus-reviewer |
try-fix-3 |
|---|---|---|---|
| Regression test passes | n/a | n/a | n/a |
| Build verified | ✓ (CI) | ✓ (local) | ✗ (not produced) |
| Fixes the root regression | ✓ | ✓ | n/a (no diff) |
| Coverage: NavigationPage(FlyoutPage) | ✓ | ✓ | — |
| Coverage: TabbedPage current tab | ✓ | ✓ | — |
| Coverage: TabbedPage non-current tabs | ✗ | ✓ | — |
| Coverage: ModalStack | ✗ | ✗ | — |
| Coverage: API 33–35 opt-in case | ✗ | ✗ | — |
| Layering quality | Acceptable | Acceptable | Adds one more layer with no semantic value |
| Surface area | 3 files / +98 | 3 files / +110 | Would add 4 files + axml + JNI binding |
| Risk | Low | Low | Higher (axml inflation, R8/trim) |
Winner
pr-plus-reviewer — strict superset of pr's correctness with one real coverage improvement (non-current TabbedPage children) verified by build. try-fix-3 was explicitly declined by its own investigation as no-better-than-the-PR, so per the rule "candidates that failed regression tests rank lower than those that passed," try-fix-3 ranks lowest because it produced no working candidate diff at all.
The reviewer's remaining recommendations (API 33–35 opt-in case, modal-stack walk, comment expansions, threading-contract one-liner) are posted as inline PR comments and left to the author's discretion — not required for the winning candidate.
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Issue details
When running on Android 16 (API 36), an issue occurs when Window.Page is replaced while a FlyoutPage is still open. In this scenario, the back navigation behavior becomes inconsistent—specifically, the overridden FlyoutPage.OnBackButtonPressed()method is not invoked as expected. This leads to incorrect or missing handling of the system back action after the page transition. This issue does not occur on Android versions 15, 14, and 13, where the back navigation works as expected.
Root Cause
The issue occurs because, on Android 16 predictive back, the FlyoutViewHandler does not properly clear its drawer and back-interception state when Window.Page is replaced from an open FlyoutPage. As a result, stale state persists across the page transition, preventing the final system back event from reaching the FlyoutPage.OnBackButtonPressed() override.
Description of Change
The fix involves introducing a new internal method, ReleaseDrawerCallbackBeforePageChange(), in FlyoutViewHandler (Android), called from Window.OnPageChanging before the page transition.
On Android 16 (API 36+), it releases the DrawerLayout system-back callback by disposing _pendingFragment, closing the drawer immediately (animated: false), and locking it in LockModeLockedClosed.
Additionally, DisconnectHandler disposes _pendingFragment at the start, preventing stale state and ensuring correct back navigation behavior.
Why Tests were not added:
Regarding the test case: Automated tests are currently executed only on Android API 30. Since this issue is specific to Android 16 (API 36), it cannot be fully validated through the existing automation setup.
Tested the behavior in the following platforms.
Issues Fixed
Fixes #33508
Output
33508-BeforeFix.mov
33508-AfterFix.mov