[Android] Fix page not disposed on Shell replace navigation#33426
[Android] Fix page not disposed on Shell replace navigation#33426PureWeen merged 9 commits intodotnet:inflight/currentfrom
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.
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. |
🤖 AI Summary📊 Expand Full Review🔍 Pre-Flight — Context & Validation📝 Review Session — Update ShellContentFragment.cs ·
|
| File | Type | Changes |
|---|---|---|
src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs |
Fix | +15 lines |
src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs |
Fix | +5 lines |
src/Controls/tests/TestCases.HostApp/Issues/Issue25134.cs |
Test (HostApp) | +114 lines |
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25134.cs |
Test (NUnit) | +27 lines |
Key Findings
- PR adds
DisposePage()method inShellContentFragmentthat callsDestroy(),DisconnectHandlers(), and nullifies the page reference DisposePage()is called fromShellItemRendererBaseonly in theShellNavigationSource.Removecase- Guard
if (_destroyed) return;prevents double-dispose (guard was added per Copilot reviewer feedback) - Copilot reviewer noted (suppressed, low confidence): Pop case and
RemoveAllPushedPagesalso don't callDisposePage()— but the issue specifically reports the Remove/replace navigation scenario - Prior agent review in PR comments: all phases complete, recommendation: APPROVE
PR Discussion
- Copilot reviewer suggested
_destroyedguard — author updated accordingly kubafloapproved the PR- No objections or blocking concerns raised
Potential Code Issues Identified
- Test timing concern: NUnit test captures
buttonelement, taps it, then assertsbutton.GetText(). The text update is async — could be flaky if assertion races the update. DisposePage()called even if fragment is_currentFragment: In the Remove case,DisposePage()is called unconditionally even whenremoveFragment == _currentFragment. In practice this shouldn't occur but is an edge case.- Pop/PopToRoot not fixed:
ShellNavigationSource.PopandRemoveAllPushedPagesdon't callDisposePage(). This PR scopes the fix to the reported scenario only.
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #33426 | Add DisposePage() in ShellContentFragment; call from Remove case in ShellItemRendererBase |
⏳ PENDING (Gate) | ShellContentFragment.cs (+15), ShellItemRendererBase.cs (+5) |
Original PR |
🚦 Gate — Test Verification
📝 Review Session — Update ShellContentFragment.cs · fb88c5a
Result: ✅ PASSED
Platform: android
Mode: Full Verification (verify-tests-fail-without-fix skill)
| Check | Expected | Actual | Result |
|---|---|---|---|
| Tests WITHOUT fix | FAIL | FAIL | ✅ |
| Tests WITH fix | PASS | PASS | ✅ |
- Tests FAIL without fix ✅
- Tests PASS with fix ✅
Test: ShellReplaceDisposesPage in Issue25134 — passed in 3s with fix on Android.
🔧 Fix — Analysis & Comparison
📝 Review Session — Update ShellContentFragment.cs · fb88c5a
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix (claude-sonnet-4.5) | Add DisconnectHandlers() call to existing Destroy() method in ShellContentFragment |
❌ FAIL | ShellContentFragment.cs (+1) |
Fragment OnDestroy() not triggered synchronously during Remove navigation |
| 2 | try-fix (claude-opus-4.6) | Call removeFragment.Fragment.Dispose() in Remove case + add DisconnectHandlers() to Dispose(bool) |
✅ PASS | ShellContentFragment.cs (+1), ShellItemRendererBase.cs (+3) |
Works by triggering full Dispose chain synchronously |
| 3 | try-fix (gpt-5.2) | Call ChildFragmentManager.ExecutePendingTransactions() after CommitAllowingStateLossEx() on Remove |
❌ FAIL | ShellItemRendererBase.cs (+4) |
Fragment lifecycle still not triggered promptly enough |
| 4 | try-fix (gpt-5.2-codex) | Call page.DisconnectHandlers() directly in Remove case (without fragment involvement) |
❌ FAIL | ShellItemRendererBase.cs (+4) |
Disconnects handlers but doesn't release page references in time |
| 5 | try-fix (gemini-3-pro-preview) | Make Destroy() internal + enhance to null _page.Handler and _page; call shellFragment.Destroy() from Remove case |
✅ PASS | ShellContentFragment.cs (+6), ShellItemRendererBase.cs (+3) |
Similar to PR but reuses existing method instead of new API |
| PR | PR #33426 | Add new DisposePage() method in ShellContentFragment (calls Destroy() + DisconnectHandlers() + nullifies _page); call from Remove case |
✅ PASS (Gate) | ShellContentFragment.cs (+15), ShellItemRendererBase.cs (+5) |
Original PR — clear, explicit, _destroyed guard included |
Cross-Pollination Round 1:
| Model | Response |
|---|---|
| claude-sonnet-4.5 | NEW IDEAS: setMaxLifecycle(DESTROYED), OnDestroyView override, FragmentLifecycleCallbacks |
| claude-opus-4.6 | NO NEW IDEAS |
| gpt-5.2 | NEW IDEA: WeakReference for _page field |
| gpt-5.2-codex | NEW IDEA: FragmentManager.FragmentLifecycleCallbacks |
| gemini-3-pro-preview | NO NEW IDEAS |
Cross-pollination attempts 6+ skipped: Rate limit hit (429 errors). Environment blocker per SHARED-RULES.md protocol.
Exhausted: No (rate limited before completing round 2), but 3 working solutions found.
Selected Fix: PR's fix — The PR's DisposePage() approach is the clearest and most intentional solution. It:
- Has an explicit
_destroyedguard preventing double-dispose - Calls
DisconnectHandlers()which properly tears down the full handler tree - Introduces a clearly named method (
DisposePage) with obvious intent - Was already refined per Copilot reviewer feedback (added
_destroyedguard) - Is slightly more defensive than attempt 2 (explicit guard vs relying on Dispose/disposed logic)
Compared to attempt 2 (Fragment.Dispose()), the PR's approach is safer because callingFragment.Dispose()on Android fragments can have unpredictable side effects and isn't the intended way to clean up a MAUI page. The PR's explicitDisposePage()method is a better API surface.
📋 Report — Final Recommendation
📝 Review Session — Update ShellContentFragment.cs · fb88c5a
✅ Final Recommendation: APPROVE
Summary
PR #33426 correctly fixes a real memory leak in Android Shell replace navigation. When using GoToAsync("../otherpage"), the replaced page's handler was not disposed, preventing garbage collection. The fix adds an explicit DisposePage() method in ShellContentFragment that is called synchronously from the ShellNavigationSource.Remove case in ShellItemRendererBase.
Root Cause
Android Shell uses ShellContentFragment to host each page. When a page is removed via replace navigation (ShellNavigationSource.Remove), the fragment is removed from the ChildFragmentManager, but the Android fragment's OnDestroy() lifecycle is not triggered synchronously. As a result, the fragment retains a strong reference to the page and its handlers, preventing GC. The fix intervenes at exactly the right point — synchronously in the Remove case — to release these references explicitly.
Fix Quality
- Approach: Dedicated
DisposePage()method inShellContentFragment— clear intent, well-scoped - Guard:
if (_destroyed) return;prevents double-dispose (added per Copilot reviewer feedback) - Disposal chain:
Destroy()(cleans UI resources) →DisconnectHandlers()(releases event handlers/native resources) →_page = null(releases reference) - Integration point: Called right after
_fragmentMap.Remove(page)in Remove case — correct timing
Alternative Fixes Explored (try-fix)
2 passing alternatives found during Phase 3:
- Attempt 2 ✅:
Fragment.Dispose()in Remove case +DisconnectHandlers()inDispose(bool)— works but callingFragment.Dispose()directly is riskier than the PR's explicit method - Attempt 5 ✅: Make
Destroy()internal + enhance to null_page/handler + call from Remove case — very similar to PR's fix, slightly less explicit naming
The PR's DisposePage() approach is the clearest and most intentional. The explicit _destroyed guard makes it slightly more defensive than alternatives.
Minor Observations
- NUnit test timing: The test captures
buttonviaWaitForElement, taps, then callsbutton.GetText(). Since Appium elements are live proxies, this works — confirmed passing in Gate. Acceptable pattern. - Scope limitation: Fix only addresses
ShellNavigationSource.Remove. ThePopcase andRemoveAllPushedPages(PopToRoot) also don't callDisposePage(). The Copilot reviewer flagged this (low confidence). This PR correctly scopes to the reported scenario; follow-up issues could address Pop/PopToRoot if needed. - Missing newline at EOF in test files — minor style issue, not blocking.
Tests
- Gate: ✅ PASSED (android, full verification — fails without fix, passes with fix)
- Test correctly uses
WeakReference<Page>+GC.Collect()to detect the leak
📋 Expand PR Finalization Review
Title: ✅ Good
Current: [Android] Fix page not disposed on Shell replace navigation
Description: ✅ Good
Description needs updates. See details below.
✨ Suggested PR Description
[!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 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's handlers remain connected and references are retained, preventing garbage collection and causing memory accumulation over repeated navigations.
Root Cause
On Android, Shell uses Fragments to display pages. During replace navigation (GoToAsync("../otherpage")), the ShellNavigationSource.Remove event fires for the page being replaced. The fragment was removed from the _fragmentMap and from ChildFragmentManager, but DisconnectHandlers() was never called on the page. The existing Destroy() path only called _page.Handler = null (for ShellContent fragments), which does not recursively disconnect the full element tree — leaving handler references intact and the page ineligible for GC.
Description of Change
ShellContentFragment.cs — Added DisposePage() method:
- Checks
_destroyedguard to prevent double-cleanup - Calls
Destroy()to clean up UI resources (views, toolbar, appearance trackers) - Calls
_page.DisconnectHandlers()to recursively disconnect all handlers in the element tree - Sets
_page = nullto break the reference
ShellItemRendererBase.cs — In HandleFragmentUpdate, case ShellNavigationSource.Remove:
- After removing the page from
_fragmentMap, casts the fragment toShellContentFragmentand callsDisposePage() - This eagerly cleans up page resources before the fragment transaction commits (safe because
Removenavigation has no exit animation, and the_destroyedflag makes the subsequentDestroy()call fromOnDestroy()a no-op)
Issues Fixed
Fixes #25134
Tested the behavior in the following platforms
- Android
- Windows
- iOS
- Mac
Code Review: ✅ Passed
Code Review Findings — PR #33426
Files reviewed:
src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cssrc/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cssrc/Controls/tests/TestCases.HostApp/Issues/Issue25134.cssrc/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25134.cs
✅ Looks Good
-
Guard against double-disposal:
DisposePage()correctly checksif (_destroyed) return;at the top, preventing any double-cleanup if the Fragment's Android lifecycle has already triggeredDestroy(). The existingDispose(bool)uses a separate_disposedflag, andOnDestroy()→Destroy()is a no-op afterDisposePage()sets_destroyed = true. All three disposal paths coexist safely. -
DisconnectHandlers()is the right call:Destroy()already calls_page.Handler = nullfor ShellContent fragments, but this doesn't recursively disconnect the full element tree.DisposePage()correctly follows with_page.DisconnectHandlers()to walk the tree and release all handler references — this is exactly what breaks the retain cycle. -
Correct placement in Remove case:
DisposePage()is called after_fragmentMap.Remove(page)but before the fragment transaction commits (CommitAllowingStateLossExis deferred). In the typical replace scenario (removeFragment == _currentFragment), the same transaction that removes the departing fragment also adds the replacement — the departing fragment's views are cleaned up before the transition, butRemovenavigation has no exit animation (SetupAnimationonly handles Push/Pop/PopToRoot), so there is no visual glitch. -
Test pattern: Using
WeakReference<Page>+GC.Collect()+GC.WaitForPendingFinalizers()is the established pattern in MAUI for verifying page disposal. The test correctly mirrors the reproduction steps from the original issue. -
[Category(UITestCategories.Shell)]: Correct single category for Shell navigation tests.
🟡 Medium: Test Assertion Timing — Potential Flakiness
File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25134.cs, line 24–26
Problem:
var button = App.WaitForElement("checkReference");
App.Tap("checkReference");
Assert.That(button.GetText(), Is.EqualTo("gone"));The button reference is captured before tapping. After App.Tap("checkReference"), the async command inside the button runs GC.Collect() → GC.WaitForPendingFinalizers() → await Task.Yield() → updates checkRefButton.Text. There is a potential race: the Assert.That(button.GetText(), ...) call may execute before the async command's Task.Yield() continuation has updated the button text.
Recommendation: Wait for the button text to change after the tap, either by re-querying the element or adding a small wait:
App.Tap("checkReference");
App.WaitForElement(AppiumQuery.ById("checkReference")); // re-wait after state change
Assert.That(App.FindElement("checkReference").GetText(), Is.EqualTo("gone"));Or simply re-fetch with WaitForElement after the tap — which returns the current state.
Risk: May pass consistently on fast devices but fail intermittently on slow emulators/CI.
🟡 Medium: DisposePage() Called Unconditionally for All Fragment Types
File: src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRendererBase.cs, lines 170–174
Problem:
if (removeFragment is ShellContentFragment shellFragment)
{
shellFragment.DisposePage();
}DisposePage() is called regardless of whether RemoveFragment() was invoked above it. Specifically, for the current-tab case (isForCurrentTab = true), RemoveFragment() is NOT called but DisposePage() IS called. In this path, the fragment is still technically in ChildFragmentManager (hidden) when DisposePage() → Destroy() destroys its UI resources. The subsequent fragment transaction (below the switch) then calls t.RemoveEx(_currentFragment.Fragment) to properly remove it.
This is the intended behavior and is safe because:
Removenavigation has no exit animation (no visual glitch)- The
_destroyedflag makes subsequentDestroy()calls fromOnDestroy()no-ops - The transaction's
CommitAllowingStateLossExis deferred, so the fragment removal is clean
Recommendation: A comment explaining the ordering rationale would improve maintainability:
// DisposePage is called here (before the transaction below removes the fragment)
// to explicitly disconnect handlers and clean up page resources. This is safe
// because Remove navigation has no exit animation, and _destroyed guards Destroy()
// against double-invocation when OnDestroy() is later called by the Android lifecycle.
if (removeFragment is ShellContentFragment shellFragment)
{
shellFragment.DisposePage();
}🔵 Low: Missing Newline at End of Both Test Files
Files:
src/Controls/tests/TestCases.HostApp/Issues/Issue25134.cs(ends without\n)src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue25134.cs(ends without\n)
Recommendation: Add a trailing newline to both files. Standard C# style.
🔵 Low: Fragile Cast Chain in OnParentSet (Test Code)
File: src/Controls/tests/TestCases.HostApp/Issues/Issue25134.cs, line 69–70
var initialPage = (section.CurrentItem as IShellContentController).Page as Issue25134InitialPage;
initialPage!.ChildPageReference = new WeakReference<Page>(this);The null-forgiving operator ! suppresses the warning but does not guard against a NullReferenceException if section.CurrentItem is null, does not implement IShellContentController, or if the Page is not an Issue25134InitialPage. While acceptable in test code where the scenario is known, a proper null check would make the failure message more meaningful:
var initialPage = (section.CurrentItem as IShellContentController)?.Page as Issue25134InitialPage;
if (initialPage is not null)
initialPage.ChildPageReference = new WeakReference<Page>(this);This is test scaffolding only; no production impact.
<!-- Please let the below note in for people that find this PR --> > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### 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 - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Issues Fixed Fixes #25134 ### Screenshots | Before Issue Fix | After Issue Fix | |----------|----------| | <video width="300" height="600" src="https://github.com/user-attachments/assets/e1b35b27-1144-45be-a522-77fbb07bae02"> | <video width="300" height="600" src="https://github.com/user-attachments/assets/bb450c26-4459-43ac-9a4f-b92fed0cefdd">) |
<!-- Please let the below note in for people that find this PR --> > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### 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 - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Issues Fixed Fixes #25134 ### Screenshots | Before Issue Fix | After Issue Fix | |----------|----------| | <video width="300" height="600" src="https://github.com/user-attachments/assets/e1b35b27-1144-45be-a522-77fbb07bae02"> | <video width="300" height="600" src="https://github.com/user-attachments/assets/bb450c26-4459-43ac-9a4f-b92fed0cefdd">) |
<!-- Please let the below note in for people that find this PR --> > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### 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 - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Issues Fixed Fixes #25134 ### Screenshots | Before Issue Fix | After Issue Fix | |----------|----------| | <video width="300" height="600" src="https://github.com/user-attachments/assets/e1b35b27-1144-45be-a522-77fbb07bae02"> | <video width="300" height="600" src="https://github.com/user-attachments/assets/bb450c26-4459-43ac-9a4f-b92fed0cefdd">) |
<!-- Please let the below note in for people that find this PR --> > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### 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 - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Issues Fixed Fixes #25134 ### Screenshots | Before Issue Fix | After Issue Fix | |----------|----------| | <video width="300" height="600" src="https://github.com/user-attachments/assets/e1b35b27-1144-45be-a522-77fbb07bae02"> | <video width="300" height="600" src="https://github.com/user-attachments/assets/bb450c26-4459-43ac-9a4f-b92fed0cefdd">) |
…3426) <!-- Please let the below note in for people that find this PR --> > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### 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 - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Issues Fixed Fixes dotnet#25134 ### Screenshots | Before Issue Fix | After Issue Fix | |----------|----------| | <video width="300" height="600" src="https://github.com/user-attachments/assets/e1b35b27-1144-45be-a522-77fbb07bae02"> | <video width="300" height="600" src="https://github.com/user-attachments/assets/bb450c26-4459-43ac-9a4f-b92fed0cefdd">) |
<!-- Please let the below note in for people that find this PR --> > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### 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 - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Issues Fixed Fixes #25134 ### Screenshots | Before Issue Fix | After Issue Fix | |----------|----------| | <video width="300" height="600" src="https://github.com/user-attachments/assets/e1b35b27-1144-45be-a522-77fbb07bae02"> | <video width="300" height="600" src="https://github.com/user-attachments/assets/bb450c26-4459-43ac-9a4f-b92fed0cefdd">) |
## What's Coming .NET MAUI inflight/candidate introduces significant improvements across all platforms with focus on quality, performance, and developer experience. This release includes 46 commits with various improvements, bug fixes, and enhancements. ## Button - [Android] Implemented material3 support for Button by @Dhivya-SF4094 in #33173 <details> <summary>🔧 Fixes</summary> - [Implement Material3 support for Button](#33172) </details> ## CollectionView - [Android] Fix RemainingItemsThresholdReachedCommand not firing when CollectionView has Header and Footer both defined by @SuthiYuvaraj in #29618 <details> <summary>🔧 Fixes</summary> - [Android : RemainingItemsThresholdReachedCommand not firing when CollectionVew has Header and Footer both defined](#29588) </details> - [iOS/MacCatalyst] Fix CollectionView ScrollTo for horizontal layouts by @Shalini-Ashokan in #33853 <details> <summary>🔧 Fixes</summary> - [[iOS/MacCatalyst] CollectionView ScrollTo does not work with horizontal Layout](#33852) </details> - [iOS & Mac] Fixed IndicatorView Size doesnt update dynamically by @SubhikshaSf4851 in #31129 <details> <summary>🔧 Fixes</summary> - [[iOS, Catalyst] IndicatorView.IndicatorSize does not update dynamically at runtime](#31064) </details> - [Android] Fix for CollectionView Scrolled event is triggered on the initial app load. by @BagavathiPerumal in #33558 <details> <summary>🔧 Fixes</summary> - [[Android] CollectionView Scrolled event is triggered on the initial app load.](#33333) </details> - [iOS, Android] Fix for CollectionView IsEnabled=false allows touch interactions by @praveenkumarkarunanithi in #31403 <details> <summary>🔧 Fixes</summary> - [More issues with CollectionView IsEnabled, InputTransparent, Opacity via Styles and code behind](#19771) </details> - [iOS] Fix VerticalOffset Update When Modifying CollectionView.ItemsSource While Scrolled by @devanathan-vaithiyanathan in #34153 <details> <summary>🔧 Fixes</summary> - [[iOS]VerticalOffset Not Reset to Zero After Clearing ItemSource in CollectionView](#26798) </details> ## DateTimePicker - [Android] Fix DatePicker MinimumDate/MaximumDate not updating dynamically by @HarishwaranVijayakumar in #33687 <details> <summary>🔧 Fixes</summary> - [[regression/8.0.3] [Android] DatePicker control minimum date issue](#19256) - [[Android] DatePicker does not update MinimumDate / MaximumDate in the Popup when set in the viewmodel after first opening](#33583) </details> ## Drawing - Android drawable perf by @albyrock87 in #31567 ## Editor - [Android] Implemented material3 support for Editor by @SyedAbdulAzeemSF4852 in #33478 <details> <summary>🔧 Fixes</summary> - [Implement Material3 Support for Editor](#33476) </details> ## Entry - [iOS, Mac] Fix for CursorPosition not updating when typing into Entry control by @SyedAbdulAzeemSF4852 in #30505 <details> <summary>🔧 Fixes</summary> - [Entry control CursorPosition does not update on TextChanged event [iOS Maui 8.0.7] ](#20911) - [CursorPosition not calculated correctly on behaviors events for iOS devices](#32483) </details> ## Flyoutpage - [Android, Windows] Fix for FlyoutPage toolbar button not updating on orientation change by @praveenkumarkarunanithi in #31962 <details> <summary>🔧 Fixes</summary> - [Flyout page in Android does not show flyout button (burger) consistently](#24468) </details> - Fix for First Item in CollectionView Overlaps in FlyoutPage.Flyout on iOS by @praveenkumarkarunanithi in #29265 <details> <summary>🔧 Fixes</summary> - [[iOS] CollectionView not rendering first item correctly in FlyoutPage.Flyout](#29170) </details> ## Image - [Android] Fix excessive memory usage for stream and resource-based image loading by @Shalini-Ashokan in #33590 <details> <summary>🔧 Fixes</summary> - [[Android] Unexpected high Bitmap.ByteCount when loading image via ImageSource.FromResource() or ImageSource.FromStream() in .NET MAUI](#33239) </details> - [Android] Fix for Resize method returns an image that has already been disposed by @SyedAbdulAzeemSF4852 in #29964 <details> <summary>🔧 Fixes</summary> - [In GraphicsView, the Resize method returns an image that has already been disposed](#29961) - [IIMage.Resize bugged behaviour](#31103) </details> ## Label - Fixed Label Span font property inheritance when applied via Style by @SubhikshaSf4851 in #34110 <details> <summary>🔧 Fixes</summary> - [`Span` does not inherit text styling from `Label` if that styling is applied using `Style` ](#21326) </details> - [Android] Implemented material3 support for Label by @SyedAbdulAzeemSF4852 in #33599 <details> <summary>🔧 Fixes</summary> - [Implement Material3 Support for Label](#33598) </details> ## Map - [Android] Fix Circle Stroke color is incorrectly updated as Fill color. by @NirmalKumarYuvaraj in #33643 <details> <summary>🔧 Fixes</summary> - [[Android] Circle Stroke color is incorrectly updated as Fill color.](#33642) </details> ## Mediapicker - [iOS] Fix: invoke MediaPicker completion handler after DismissViewController by @yuriikyry4enko in #34250 <details> <summary>🔧 Fixes</summary> - [[iOS] Media Picker UIImagePickerController closing issue](#21996) </details> ## Navigation - Fix ContentPage memory leak on Android when using NavigationPage modally (fixes #33918) by @brunck in #34117 <details> <summary>🔧 Fixes</summary> - [[Android] Modal TabbedPage whose tabs are NavigationPage(ContentPage) is retained after PopModalAsync()](#33918) </details> ## Picker - [Android] Implement material3 support for TimePicker by @HarishwaranVijayakumar in #33646 <details> <summary>🔧 Fixes</summary> - [Implement Material3 support for TimePicker](#33645) </details> - [Android] Implemented Material3 support for Picker by @SyedAbdulAzeemSF4852 in #33668 <details> <summary>🔧 Fixes</summary> - [Implement Material3 support for Picker](#33665) </details> ## RadioButton - [Android] Implemented material3 support for RadioButton by @SyedAbdulAzeemSF4852 in #33468 <details> <summary>🔧 Fixes</summary> - [Implement Material3 Support for RadioButton](#33467) </details> ## Setup - Clarify MA003 error message by @jeremy-visionaid in #34067 <details> <summary>🔧 Fixes</summary> - [MA003 false positive with 9.0.21](#26599) </details> ## Shell - [Android] Fix TabBar FlowDirection not updating dynamically by @SubhikshaSf4851 in #33091 <details> <summary>🔧 Fixes</summary> - [[Android, iOS] FlowDirection RTL is not updated dynamically on Shell TabBar](#32993) </details> - [Android] Fix page not disposed on Shell replace navigation by @Vignesh-SF3580 in #33426 <details> <summary>🔧 Fixes</summary> - [[Android] [Shell] replace navigation leaks current page](#25134) </details> - [Android] Fixed Shell flyout does not disable scrolling when FlyoutVerticalScrollMode is set to Disabled by @NanthiniMahalingam in #32734 <details> <summary>🔧 Fixes</summary> - [[Android] Shell.FlyoutVerticalScrollMode="Disabled" does not disable scrolling](#32477) </details> ## Single Project - Fix: Throw a clear error when an SVG lacks dimensions instead of a NullReferenceException by @Shalini-Ashokan in #33194 <details> <summary>🔧 Fixes</summary> - [MAUI Fails To Convert Valid SVG Files Into PNG Files (Object reference not set to an instance of an object)](#32460) </details> ## SwipeView - [iOS] Fix SwipeView stays open on iOS after updating content by @devanathan-vaithiyanathan in #31248 <details> <summary>🔧 Fixes</summary> - [[iOS] - Swipeview with collectionview issue](#19541) </details> ## TabbedPage - [Windows] Fixed IsEnabled Property not works on Tabs by @NirmalKumarYuvaraj in #26728 <details> <summary>🔧 Fixes</summary> - [ShellContent IsEnabledProperty does not work](#5161) - [[Windows] Shell Tab IsEnabled Not Working](#32996) </details> - [Android] Fix NavigationBar overlapping StatusBar when NavigationBar visibility changes by @Vignesh-SF3580 in #33359 <details> <summary>🔧 Fixes</summary> - [[Android] NavigationBar overlaps with StatusBar when mixing HasNavigationBar=true/false in TabbedPage on Android 15 (API 35)](#33340) </details> ## Templates - Fix for unable to open task using keyboard navigation on windows platform by @SuthiYuvaraj in #33647 <details> <summary>🔧 Fixes</summary> - [Unable to open task using keyboard: A11y_.NET maui_User can get all the insights of Dashboard_Keyboard](#30787) </details> ## TitleView - Fix for NavigationPage.TitleView does not expand with host window in iPadOS 26+ by @SuthiYuvaraj in #33088 ## Toolbar - [iOS] Fix toolbar items ignoring BarTextColor on iOS/MacCatalyst 26+ by @Shalini-Ashokan in #34036 <details> <summary>🔧 Fixes</summary> - [[iOS 26] ToolbarItem color with custom BarTextColor not working](#33970) </details> - [Android] Fix for ToolbarItem retaining the icon from the previous page on Android when using NavigationPage. by @BagavathiPerumal in #32311 <details> <summary>🔧 Fixes</summary> - [Toolbaritem keeps the icon of the previous page on Android, using NavigationPage (not shell)](#31727) </details> ## WebView - [Android] Fix WebView in a grid expands beyond it's cell by @devanathan-vaithiyanathan in #32145 <details> <summary>🔧 Fixes</summary> - [Android - WebView in a grid expands beyond it's cell](#32030) </details> ## Xaml - ContentPresenter: Propagate binding context to children with explicit TemplateBinding by @HarishwaranVijayakumar in #30880 <details> <summary>🔧 Fixes</summary> - [Binding context in ContentPresenter](#23797) </details> <details> <summary>🔧 Infrastructure (1)</summary> - [Revert] ContentPresenter: Propagate binding context to children with explicit TemplateBinding by @Ahamed-Ali in #34332 </details> <details> <summary>🧪 Testing (6)</summary> - [Testing] Feature Matrix UITest Cases for Shell Flyout Page by @NafeelaNazhir in #32525 - [Testing] Feature Matrix UITest Cases for Brushes by @LogishaSelvarajSF4525 in #31833 - [Testing] Feature Matrix UITest Cases for BindableLayout by @LogishaSelvarajSF4525 in #33108 - [Android] Add UI tests for Material 3 CheckBox by @HarishwaranVijayakumar in #34126 <details> <summary>🔧 Fixes</summary> - [[Android] Add UI tests for Material 3 CheckBox](#34125) </details> - [Testing] Feature Matrix UITest Cases for Shell Tabbed Page by @NafeelaNazhir in #33159 - [Testing] Fixed Test case failure in PR 34294 - [03/2/2026] Candidate - 1 by @TamilarasanSF4853 in #34334 </details> <details> <summary>📦 Other (2)</summary> - Bumps Syncfusion.Maui.Toolkit dependency to version 1.0.9 by @PaulAndersonS in #34178 - Fix crash when closing Windows based app when using TitleBar by @MFinkBK in #34032 <details> <summary>🔧 Fixes</summary> - [Unhandled exception "Value does not fall within the expected range" when closing Windows app](#32194) </details> </details> **Full Changelog**: main...inflight/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 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