-
Notifications
You must be signed in to change notification settings - Fork 1.9k
[Android] Fix page not disposed on Shell replace navigation #33426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
PR Review: #33426 - [Android] Fix page not disposed on Shell replace navigationDate: 2026-01-16 | Issue: #25134 | PR: #33426 ✅ Final Recommendation: APPROVE
📋 Issue SummaryIssue #25134: [Android] [Shell] replace navigation leaks current pageReported by: @albyrock87 (Contributor) Problem DescriptionShell is leaking the page on Android upon replace navigation. When using Shell's Root Cause (from PR)On Android, Shell uses Fragments to display pages. During replace navigation, a new page is pushed while the old page is popped, but references to the old page are still retained. Because the page handler and associated resources are not explicitly disposed, pages can accumulate in memory over time. Steps to Reproduce
Expected: Page should be garbage collected after replacement Platforms Affected:
Regression Info:
Workaround: Additional Context:
📁 Files Changed
Total changes: +157 lines (all additions, no deletions) Test Type: UI Tests (Appium) with memory leak detection via WeakReference 💬 PR Discussion SummaryPR Comments:
Issue Comments:
Inline Code Review Comments:
Author Uncertainty:
Disagreements to Investigate:
Edge Cases to Investigate:
🧪 TestsStatus: ✅ COMPLETE (User verified in Sandbox)
Test Files:
Test Scenario (from Sandbox):
Flow:
🚦 Gate - Test VerificationStatus: ✅ PASSED (User verified)
Result: ✅ PASSED - User confirmed fix properly disposes pages during replace navigation 🔧 Fix CandidatesStatus: ✅ COMPLETE
PR's Approach Summary (from description):
Note: try-fix candidates (1, 2, 3...) will be added during Phase 4 after Gate passes. Exhausted: Yes (all viable alternatives evaluated) 📋 Final Review Summary✅ APPROVAL - PR #33426What This PR Fixes:
Fix Implementation:
Verification:
Why This Approach is Optimal:
Alternatives Considered:
Code Quality:
Recommendation: ✅ APPROVE - Ready to merge Review Completed: 2026-01-16 |
|
/azp run maui-pr-uitests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
/azp run maui-pr-devicetests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request fixes a memory leak in Shell navigation on Android where pages are retained in memory after being replaced during navigation (e.g., GoToAsync("../otherpage")). The issue occurs because page handlers are not properly disposed when fragments are removed from the navigation stack.
Changes:
- Added a new
DisposePage()method to explicitly clean up page resources during Android Shell navigation - Modified the
ShellNavigationSource.Removecase to callDisposePage()on removed fragments - Added UI tests to verify that replaced pages are properly garbage collected
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs |
Added internal DisposePage() method that calls Destroy() and explicitly disconnects handlers and nullifies the page reference |
src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs |
Modified ShellNavigationSource.Remove case to call DisposePage() on removed ShellContentFragment instances |
src/Controls/tests/TestCases.HostApp/Issues/Issue25134.cs |
Added test pages demonstrating replace navigation and verifying WeakReference collection via GC |
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25134.cs |
Added NUnit UI test that validates page disposal through weak reference verification |
Comments suppressed due to low confidence (2)
src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs:162
- The
ShellNavigationSource.Popcase should also callDisposePage()to prevent memory leaks. Currently only theRemovecase disposes pages, butPopnavigation (going back) can also leave pages in memory. This inconsistency means that normal back navigation will still leak memory. Consider adding the same disposal logic here as in theRemovecase.
case ShellNavigationSource.Pop:
if (_fragmentMap.TryGetValue(page, out var frag))
{
if (ChildFragmentManager.Contains(frag.Fragment) && !isForCurrentTab)
RemoveFragment(frag.Fragment);
_fragmentMap.Remove(page);
}
if (!isForCurrentTab)
return Task.FromResult(true);
break;
src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs:428
- The
RemoveAllPushedPagesmethod (used byPopToRoot) should also callDisposePage()on each removedShellContentFragment. This method removes multiple pages at once but doesn't dispose them, which would cause memory leaks when navigating to root. Add disposal logic in the loop before or aftert.RemoveEx(kvp.Value.Fragment).
void RemoveAllPushedPages(ShellSection shellSection, bool keepCurrent)
{
if (shellSection.Stack.Count <= 1 || (keepCurrent && shellSection.Stack.Count == 2))
return;
var t = ChildFragmentManager.BeginTransactionEx();
foreach (var kvp in _fragmentMap.ToList())
{
if (kvp.Key.Parent != shellSection)
continue;
_fragmentMap.Remove(kvp.Key);
if (keepCurrent && kvp.Value.Fragment == _currentFragment)
continue;
t.RemoveEx(kvp.Value.Fragment);
}
t.CommitAllowingStateLossEx();
}
src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs
Show resolved
Hide resolved
|
@sheiksyedm why is simple Pop navigation not leaking the page like Replace? I mean, on Pop I don't see a "DisposePage" call, so there must be something else there which is taking care of disconnecting the handler. Shouldn't we use the same approach Pop uses? |
@albyrock87 Pop navigation not leaking the page like Replace is Pop Navigation: When using Pop, the fragment is removed from the FragmentManager transaction( t.RemoveEx(_currentFragment.Fragment) at line 250 in ShellItemRendererBase.cs), So Pop navigation automatically cleans up through Android's Fragment lifecycle. Replace Navigation: With Remove/Replace navigation (e.g., GoToAsync("../otherpage")), Shell generates TWO sequential navigation events: The problem is old page's fragment stays in FragmentManager, its lifecycle never completes, and DisconnectHandlers() is never called. The PR's fix explicitly calls DisposePage() ensures cleanup happens synchronously during the Remove operation. |
|
@sheiksyedm thanks for explaining.. So should we call OnDestroyView() instead to mimic what's happening on Pop? Or is it not possible for some reason? I was just trying to not add a second cleanup code path, though if there's no other way I get it. Thank you for fixing the leak! |
|
@albyrock87 The dual cleanup path is necessary because the Fragment lifecycle path is unreliable for Replace navigation due to timing issues and early returns when conditions fail. The explicit DisposePage() call ensures that cleanup happens regardless of the fragment state. |
Issue Detail
When using Shell's GoToAsync() with replace navigation (e.g.,
GoToAsync("../otherpage")), the removed page is not properly cleaned up on Android. The page handler remains referenced, preventing garbage collection and causing memory accumulation over navigations.Root Cause
On Android, Shell uses Fragments to display pages. During replace navigation, a new page is pushed while the old page is popped, but references to the old page are still retained. Because the page handler and associated resources are not explicitly disposed, pages can accumulate in memory over time
Description of Change
When Remove occurs, the code now directly calls DisposePage() on the associated ShellContentFragment. The DisposePage method calls Destroy to clean up UI resources, invokes DisconnectHandlers on the page, and nullifies the page reference.
Tested the behavior in the following platforms
Issues Fixed
Fixes #25134
Screenshots
Before25134.mov
After25134.mov