-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Fix memory leak in FlyoutPage when changing the Detail #33358
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
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 PR addresses memory leaks in FlyoutPage when replacing the Detail property, particularly affecting NavigationPage instances with Android fragments. The fix involves disconnecting handlers when Detail is replaced, cleaning up fragments in StackNavigationManager, and properly nullifying references in fragment lifecycle methods.
Key Changes:
- Added handler disconnection when FlyoutPage Detail is replaced
- Implemented fragment cleanup logic in StackNavigationManager.Disconnect()
- Added comprehensive memory leak tests for FlyoutPage Detail replacement scenarios
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/Controls/src/Core/FlyoutPage/FlyoutPage.cs |
Calls DisconnectHandler() on previousDetail to break handler references when Detail is replaced |
src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs |
Adds CleanUpFragments method, nullifies fields in Disconnect(), adds diagnostic finalizer, imports System.Linq |
src/Core/src/Platform/Android/Navigation/ScopedFragment.cs |
Nullifies DetailView in OnDestroy() to prevent memory leaks |
src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs |
Nullifies _navigationManager and calls Dispose() in OnDestroy() |
src/Core/src/Platform/Android/Navigation/MauiNavHostFragment.cs |
Overrides OnDestroy() to call Dispose() |
src/Controls/tests/DeviceTests/Memory/MemoryTests.cs |
Adds FlyoutViewHandler registration and two new memory leak tests for FlyoutPage Detail replacement |
Comments suppressed due to low confidence (1)
src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs:385
- The finalizer contains a Debug.WriteLine call which should be removed in production code. Finalizers should perform minimal cleanup work without logging, as they run on a separate thread and can cause performance issues. If this is temporary diagnostic code, it should be removed before merging.
{
// If the previous Navigation Host Fragment was destroyed then we need to add a new one
if (_fragmentManager.IsDestroyed(MauiContext.Context) &&
_fragmentContainerView is not null &&
_fragmentContainerView.Fragment is null)
{
| _navigationManager = null; | ||
|
|
||
| base.OnDestroy(); | ||
| this.Dispose(); |
Copilot
AI
Jan 2, 2026
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.
Calling Dispose() on a Fragment is unnecessary and potentially problematic. Android's Fragment lifecycle already handles resource cleanup through OnDestroy(). Explicitly calling Dispose() can cause issues with the Fragment lifecycle and may lead to double-disposal or premature resource cleanup. The nullification of fields in OnDestroy() is sufficient for preventing memory leaks.
| this.Dispose(); |
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.
not calling the dispose causes the memory leak
| public override void OnDestroy() | ||
| { | ||
| base.OnDestroy(); | ||
| this.Dispose(); |
Copilot
AI
Jan 2, 2026
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.
Calling Dispose() on a Fragment is unnecessary and potentially problematic. Android's Fragment lifecycle already handles resource cleanup through OnDestroy(). Explicitly calling Dispose() can cause issues with the Fragment lifecycle and may lead to double-disposal or premature resource cleanup.
| this.Dispose(); |
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.
not calling the dispose causes the memory leak
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.
Hmmm this seems wrong though, we shouldn't need to call Dispose here to fix a memory leak.
This seems like maybe the dispose is triggering something else that's possibilty fixing the leak?
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.
@PureWeen not sure... Maybe it's somehow removing it from the fragmentManager? The leaks on android specific code was related to bad clean up on FragmentManager, before trying to call dispose I did all I find to remove the navHost from the fragment manager but nothing helped. Maybe calling the dispose from StackNavigationManager would be better?
| base.OnDestroy(); | ||
| IsDestroyed = true; | ||
|
|
||
| DetailView = null!; |
Copilot
AI
Jan 2, 2026
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.
Setting DetailView to null! after base.OnDestroy() could cause issues if base.OnDestroy() or any fragment lifecycle callbacks try to access DetailView. Consider nullifying DetailView before calling base.OnDestroy() to prevent potential NullReferenceException scenarios during the fragment's destruction process.
| base.OnDestroy(); | |
| IsDestroyed = true; | |
| DetailView = null!; | |
| IsDestroyed = true; | |
| DetailView = null!; | |
| base.OnDestroy(); |
src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs
Outdated
Show resolved
Hide resolved
src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs
Outdated
Show resolved
Hide resolved
| { | ||
| previousDetail.SendNavigatedFrom( | ||
| new NavigatedFromEventArgs(destinationPage: value, NavigationType.Replace)); | ||
| previousDetail.Handler?.DisconnectHandler(); |
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.
Not 100% sure about this one, maybe the dev. should be responsible to call the DisconnectHandler?
| { | ||
| previousDetail.SendNavigatedFrom( | ||
| new NavigatedFromEventArgs(destinationPage: value, NavigationType.Replace)); | ||
| previousDetail.Handler?.DisconnectHandler(); |
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.
I think this is fine but we would want to call DisconnectHAndler after the page is unloaded
if you look at "void OnPageChanged(Page? oldPage, Page? newPage)" inside window you'll see where I wire into the unloaded event and then call disocnnecthandler from there.
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.
it will not work, that method isn't called when the FyoutPage.Detail is replaced. For reference this is the scenario that opened the issue
Pre-FlightStatus: ✅ COMPLETE
SummaryUser reports memory leaks when navigating in FlyoutPage with NavigationPages on Android. The issue reproduces by repeatedly replacing Key Disagreements
Author Uncertainty
Edge Cases to Check
Files ClassificationFix Files:
Test Files:
Phase 0: GateStatus: ✅ PASSED (with limitations)
Result: PASSED ✅ (with note) Gate AssessmentTest Structure Validation:
Limitation:
However, the test structure is sound:
Proceeding with analysis given:
Phase 1: AnalysisStatus: ✅ PASSED
Root Cause:
The issue exists because:
My Approach:
Key Difference: Phase 2: CompareStatus: ✅ PASSED
Recommendation:
Key FindingsAreas of Agreement (80% of PR):
Areas of Concern (20% of PR):
Trade-off AnalysisOption A: Approve with minor change request
Option B: Request investigation of Dispose() necessity
Recommended: Option A - the fix works, CleanUpFragments loop should be fixed, Dispose() concern should be documented but not block merge. Phase 3: RegressionStatus: ✅ PASSED Edge Cases (from pre-flight)
Disagreements Investigated
Regression Risk AssessmentLow Risk (Core fix):
Medium Risk (Specific concerns):
Mitigation: Request minor fixes + document concerns for future investigation Phase 4: ReportStatus: ✅ PASSED Executive SummaryThis PR fixes a significant memory leak in FlyoutPage when Detail is replaced with new NavigationPage instances on Android. The fix is sound in principle and empirically validated by the author, but has two minor issues that should be addressed before merge. Recommendation: ✅ APPROVE with requested changes Pre-Flight Summary (from Phase 0)
Phase 1: Root Cause AnalysisThe Problem:
My Independent Analysis:
Key Insight: PR's approach matches my analysis except for:
Phase 2: Approach Comparison
Agreement: 80% of PR aligns with best practices Phase 3: Regression Testing ResultsEdge Cases Investigated: 7 total
Disagreements Resolved: 4 total
Regression Risk: LOW with minor fixes applied Final Recommendation: ✅ APPROVE with Requested ChangesRequired Changes (Must Fix)
Suggested Investigation (Future Work)Dispose() calls on Fragments - Document for future investigation:
Recommendation: Accept the Dispose() calls for now since they're empirically proven, but add TODO comments for future investigation: // TODO: Investigate if Dispose() is truly necessary or if it's masking
// a deeper issue with fragment lifecycle. Multiple reviewers questioned
// this approach. See PR #33358 discussion.
this.Dispose();Strengths of This PR
Weaknesses of This PR
Comparison with My Alternative ApproachWhat I would do differently:
What PR does better:
Net assessment: PR is 90% correct, needs 10% refinement Technical Deep Dive: Why Dispose() Might Be NecessaryTheory: Fragment.Dispose() in .NET for Android:
Without Dispose():
With Dispose():
Conclusion: Dispose() may be necessary specifically for .NET Android binding architecture, not general Fragment lifecycle. This would explain why reviewers found it "unusual" but author found it "necessary". Action: Document this theory in code comments What Happens NextIf required changes are made:
Result: Robust fix that handles edge cases and is maintainable Final VerdictStatus: ✅ APPROVE with requested changes Confidence: HIGH (90%)
Risk: LOW
Bottom Line: This PR solves a real problem with a mostly correct approach. The two required fixes (while loop, unused import) are trivial. The Dispose() concern is valid but shouldn't block merge since it's proven to work. Approve and merge once required changes are applied. |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
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
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
| references.Add(new(detailPage.Handler)); | ||
| references.Add(new(detailPage.Handler.PlatformView)); | ||
| } | ||
|
|
Copilot
AI
Jan 3, 2026
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.
The test delays (Task.Delay(50)) appear to be used to allow async operations to complete, but this could make tests flaky. Consider using a more deterministic approach, such as waiting for specific conditions or events to complete. If the delays are necessary for the GC to run or for fragments to be destroyed, add a comment explaining why this specific delay is needed.
| // Give the platform time to complete disposal/teardown of the previous Detail | |
| // (handlers, native views, fragments) before proceeding to the next iteration. | |
| // This small delay has been found necessary to make the subsequent GC-based | |
| // memory assertions reliable across devices. |
@3sRykaert the workaround would be to not replace the |
|
Description of Change
This PR fixes several memory leaks related with NavigationPage and Android fragments.
Comparing the first and last gcdump.
Issues Fixed
Fixes #33355