From 6c9a6c7ec7b257c66efc5d34964a5d3698107c13 Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Wed, 11 Mar 2026 23:27:01 -0300 Subject: [PATCH 01/13] call DisconnectHandler --- src/Controls/src/Core/FlyoutPage/FlyoutPage.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs b/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs index 580652749e05..b2d414619f3b 100644 --- a/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs +++ b/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs @@ -76,6 +76,7 @@ public Page Detail { previousDetail.SendNavigatedFrom( new NavigatedFromEventArgs(destinationPage: value, NavigationType.Replace)); + previousDetail.Handler?.DisconnectHandler(); } _detail.SendNavigatedTo(new NavigatedToEventArgs(previousDetail, NavigationType.Replace)); From 96d33dc516b55af5d92b08cc42bc247c18110924 Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Wed, 11 Mar 2026 23:27:16 -0300 Subject: [PATCH 02/13] clean up StackNavigationManager # Conflicts: # src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs --- .../Platform/Android/Navigation/StackNavigationManager.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs index a7c298ad5067..6c616735cec2 100644 --- a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs +++ b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs @@ -310,6 +310,11 @@ public virtual void Disconnect() _fragmentContainerView.ChildViewAdded -= OnNavigationHostViewAdded; } + if (_fragmentManager is not null) + { + CleanUpFragments(_fragmentManager); + } + _fragmentLifecycleCallbacks?.Disconnect(); _fragmentLifecycleCallbacks = null; @@ -496,7 +501,10 @@ void SetNavHost(NavHostFragment? navHost) return; if (_navHost is MauiNavHostFragment oldHost) + { oldHost.StackNavigationManager = null; + oldHost.Dispose(); + } if (navHost is MauiNavHostFragment newHost) newHost.StackNavigationManager = this; From 3bf02a4842f0f7bf80fc76d4b129afe98bd9a1db Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Wed, 11 Mar 2026 23:40:51 -0300 Subject: [PATCH 03/13] Nullingout the _navigationManager After the leak fix, I revert this line to see where it impacts. The results are Without this, the `NavigationViewFragment` and `StackNavigationManager` will leak. --- .../src/Platform/Android/Navigation/NavigationViewFragment.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs index 496990dee1a7..a5fd1a999391 100644 --- a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs +++ b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs @@ -89,6 +89,7 @@ public override void OnDestroy() { _currentView = null; _fragmentContainerView = null; + _navigationManager = null; base.OnDestroy(); } From f487e20ba607d4d292f60c90a0b8f0ada995f9ab Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Fri, 13 Mar 2026 01:41:35 -0300 Subject: [PATCH 04/13] clean up NavigationViewFragment Since this object is kept alive by the Android world, we must clean all managed references held by it. --- .../src/Platform/Android/Navigation/NavigationViewFragment.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs index a5fd1a999391..d6a00ba79176 100644 --- a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs +++ b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs @@ -87,6 +87,8 @@ public override void OnResume() public override void OnDestroy() { + _fragmentContainerView?.RemoveView(_currentView); + _currentView?.RemoveFromParent(); _currentView = null; _fragmentContainerView = null; _navigationManager = null; From 05ded57d64691688b3738c41aff6c239a92d8c58 Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Fri, 13 Mar 2026 01:46:44 -0300 Subject: [PATCH 05/13] fix rebase --- .../Platform/Android/Navigation/StackNavigationManager.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs index 6c616735cec2..cff7e81145c8 100644 --- a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs +++ b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs @@ -310,11 +310,6 @@ public virtual void Disconnect() _fragmentContainerView.ChildViewAdded -= OnNavigationHostViewAdded; } - if (_fragmentManager is not null) - { - CleanUpFragments(_fragmentManager); - } - _fragmentLifecycleCallbacks?.Disconnect(); _fragmentLifecycleCallbacks = null; From b194e706440edd9f31565ccfe008b9baa463217e Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Fri, 13 Mar 2026 19:07:21 -0300 Subject: [PATCH 06/13] add memory test --- .../tests/DeviceTests/Memory/MemoryTests.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs index 5a804343d967..ed716c935885 100644 --- a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs +++ b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs @@ -89,9 +89,11 @@ void SetupBuilder() #if IOS || MACCATALYST handlers.AddHandler(); handlers.AddHandler(); + handlers.AddHandler(); #else handlers.AddHandler(); handlers.AddHandler(); + handlers.AddHandler(); #endif }); }); @@ -148,6 +150,51 @@ await CreateHandlerAndAddToWindow(new Window(navPage), async () => await AssertionExtensions.WaitForGC(references.ToArray()); } + #if ANDROID + [Fact("FlyoutPage Detail Navigation Does Not Leak")] + public async Task FlyoutPageDetailNavigationDoesNotLeak() + { + SetupBuilder(); + + var references = new List(); + + var flyoutPage = new FlyoutPage + { + Flyout = new ContentPage { Title = "Flyout" }, + Detail = new NavigationPage(new ContentPage { Title = "Initial Detail" }) + }; + + await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => + { + for (int i = 0; i < 2; i++) + { + var detailPage = new ContentPage + { + Title = $"Detail {i}", + Content = new Label { Text = $"Content {i}" } + }; + var navPage = new NavigationPage(detailPage); + + flyoutPage.Detail = navPage; + flyoutPage.IsPresented = false; + + await OnLoadedAsync(detailPage); + + references.Add(new(detailPage)); + references.Add(new(detailPage.Handler)); + references.Add(new(detailPage.Handler.PlatformView)); + references.Add(new(navPage)); + references.Add(new(navPage.Handler)); + references.Add(new(navPage.Handler.PlatformView)); + + await Task.Delay(100); + } + }); + + await AssertionExtensions.WaitForGC(references.ToArray()); + } +#endif + [Theory("Handler Does Not Leak")] [InlineData(typeof(ActivityIndicator))] [InlineData(typeof(Border))] From e856ffab42956924fa3648d4de49098fda5e06dc Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Sun, 15 Mar 2026 16:26:03 -0300 Subject: [PATCH 07/13] improve tests --- .../tests/DeviceTests/Memory/MemoryTests.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs index ed716c935885..93e9a4fe3a34 100644 --- a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs +++ b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs @@ -158,15 +158,17 @@ public async Task FlyoutPageDetailNavigationDoesNotLeak() var references = new List(); + var initialDetail = new NavigationPage(new ContentPage { Title = "Initial Detail" }); + var flyoutPage = new FlyoutPage { Flyout = new ContentPage { Title = "Flyout" }, - Detail = new NavigationPage(new ContentPage { Title = "Initial Detail" }) + Detail = initialDetail }; await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => { - for (int i = 0; i < 2; i++) + for (int i = 0; i < 4; i++) { var detailPage = new ContentPage { @@ -181,16 +183,15 @@ await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => await OnLoadedAsync(detailPage); references.Add(new(detailPage)); - references.Add(new(detailPage.Handler)); - references.Add(new(detailPage.Handler.PlatformView)); references.Add(new(navPage)); - references.Add(new(navPage.Handler)); - references.Add(new(navPage.Handler.PlatformView)); - - await Task.Delay(100); } }); + + // The last page will be alive and attached to the FlyoutPage + references.RemoveAt(references.Count - 1); + references.RemoveAt(references.Count - 1); + await AssertionExtensions.WaitForGC(references.ToArray()); } #endif From d6b1bfcc6f1a9dd033aa58aa2fbe88b6cbfa27fe Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Mon, 16 Mar 2026 21:02:56 -0300 Subject: [PATCH 08/13] revert teh oldHost.Dispose call --- .../src/Platform/Android/Navigation/StackNavigationManager.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs index cff7e81145c8..a7c298ad5067 100644 --- a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs +++ b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs @@ -496,10 +496,7 @@ void SetNavHost(NavHostFragment? navHost) return; if (_navHost is MauiNavHostFragment oldHost) - { oldHost.StackNavigationManager = null; - oldHost.Dispose(); - } if (navHost is MauiNavHostFragment newHost) newHost.StackNavigationManager = this; From 7e298212c6d07fc4f72a2fe31a7e45a24ebf52d5 Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Mon, 16 Mar 2026 21:31:57 -0300 Subject: [PATCH 09/13] code review --- .../Platform/Android/Navigation/NavigationViewFragment.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs index d6a00ba79176..8064a1e1b348 100644 --- a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs +++ b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs @@ -87,8 +87,11 @@ public override void OnResume() public override void OnDestroy() { - _fragmentContainerView?.RemoveView(_currentView); - _currentView?.RemoveFromParent(); + if (_currentView is not null) + { + _fragmentContainerView?.RemoveView(_currentView); + _currentView.RemoveFromParent(); + } _currentView = null; _fragmentContainerView = null; _navigationManager = null; From 8e5783cb01aff82b6e095f53ea7cea9efbca160f Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Wed, 18 Mar 2026 16:29:35 -0300 Subject: [PATCH 10/13] change call to DisconnetHandler --- src/Controls/src/Core/FlyoutPage/FlyoutPage.cs | 1 - .../src/Handlers/FlyoutView/FlyoutViewHandler.Android.cs | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs b/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs index b2d414619f3b..580652749e05 100644 --- a/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs +++ b/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs @@ -76,7 +76,6 @@ public Page Detail { previousDetail.SendNavigatedFrom( new NavigatedFromEventArgs(destinationPage: value, NavigationType.Replace)); - previousDetail.Handler?.DisconnectHandler(); } _detail.SendNavigatedTo(new NavigatedToEventArgs(previousDetail, NavigationType.Replace)); diff --git a/src/Core/src/Handlers/FlyoutView/FlyoutViewHandler.Android.cs b/src/Core/src/Handlers/FlyoutView/FlyoutViewHandler.Android.cs index fac7d6e86772..97f4e0eb75f5 100644 --- a/src/Core/src/Handlers/FlyoutView/FlyoutViewHandler.Android.cs +++ b/src/Core/src/Handlers/FlyoutView/FlyoutViewHandler.Android.cs @@ -70,8 +70,11 @@ void UpdateDetailsFragmentView() if (context is null) return; - if (VirtualView.Detail?.Handler is IPlatformViewHandler pvh) - pvh.DisconnectHandler(); + if (_detailViewFragment?.DetailView is IView previousDetail && + previousDetail != VirtualView.Detail) + { + previousDetail.Handler?.DisconnectHandler(); + } var fragmentManager = MauiContext.GetFragmentManager(); From bfbdb291de0ecb5c91b1b6557d98a3b85c67027b Mon Sep 17 00:00:00 2001 From: Pedro Jesus Date: Fri, 20 Mar 2026 18:26:36 -0300 Subject: [PATCH 11/13] repro for memory leak (to be reverted) --- .../Controls.Sample.Sandbox/App.xaml.cs | 12 +- .../AppFlyoutPage.xaml | 23 ++++ .../AppFlyoutPage.xaml.cs | 42 +++++++ .../Controls.Sample.Sandbox/Page1.xaml | 22 ++++ .../Controls.Sample.Sandbox/Page1.xaml.cs | 44 ++++++++ .../Controls.Sample.Sandbox/Page2.xaml | 18 +++ .../Controls.Sample.Sandbox/Page2.xaml.cs | 19 ++++ .../Services/NavigationService.cs | 103 ++++++++++++++++++ 8 files changed, 272 insertions(+), 11 deletions(-) create mode 100644 src/Controls/samples/Controls.Sample.Sandbox/AppFlyoutPage.xaml create mode 100644 src/Controls/samples/Controls.Sample.Sandbox/AppFlyoutPage.xaml.cs create mode 100644 src/Controls/samples/Controls.Sample.Sandbox/Page1.xaml create mode 100644 src/Controls/samples/Controls.Sample.Sandbox/Page1.xaml.cs create mode 100644 src/Controls/samples/Controls.Sample.Sandbox/Page2.xaml create mode 100644 src/Controls/samples/Controls.Sample.Sandbox/Page2.xaml.cs create mode 100644 src/Controls/samples/Controls.Sample.Sandbox/Services/NavigationService.cs diff --git a/src/Controls/samples/Controls.Sample.Sandbox/App.xaml.cs b/src/Controls/samples/Controls.Sample.Sandbox/App.xaml.cs index 9512dea98e39..9dc08b9101db 100644 --- a/src/Controls/samples/Controls.Sample.Sandbox/App.xaml.cs +++ b/src/Controls/samples/Controls.Sample.Sandbox/App.xaml.cs @@ -9,16 +9,6 @@ public App() protected override Window CreateWindow(IActivationState? activationState) { - // To test shell scenarios, change this to true - bool useShell = false; - - if (!useShell) - { - return new Window(new NavigationPage(new MainPage())); - } - else - { - return new Window(new SandboxShell()); - } + return new Window(new AppFlyoutPage()); } } diff --git a/src/Controls/samples/Controls.Sample.Sandbox/AppFlyoutPage.xaml b/src/Controls/samples/Controls.Sample.Sandbox/AppFlyoutPage.xaml new file mode 100644 index 000000000000..d43d0013bfc7 --- /dev/null +++ b/src/Controls/samples/Controls.Sample.Sandbox/AppFlyoutPage.xaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/src/Controls/samples/Controls.Sample.Sandbox/AppFlyoutPage.xaml.cs b/src/Controls/samples/Controls.Sample.Sandbox/AppFlyoutPage.xaml.cs new file mode 100644 index 000000000000..a0fac3aae8ee --- /dev/null +++ b/src/Controls/samples/Controls.Sample.Sandbox/AppFlyoutPage.xaml.cs @@ -0,0 +1,42 @@ +using Maui.Controls.Sample.Services; + +namespace Maui.Controls.Sample; + +public partial class AppFlyoutPage : FlyoutPage +{ + public class MenuItem + { + public string Title { get; set; } = string.Empty; + public Type PageType { get; set; } = typeof(Page); + } + + public List MenuItems { get; set; } + + public AppFlyoutPage() + { + InitializeComponent(); + + MenuItems = new List + { + new MenuItem { Title = "page 1", PageType = typeof(Page1) }, + new MenuItem { Title = "page 2", PageType = typeof(Page2) } + }; + + BindingContext = this; + + // Set default detail page + Detail = new NavigationPage(new Page1()); + } + + private void OnMenuItemSelected(object? sender, SelectionChangedEventArgs e) + { + if (e.CurrentSelection.FirstOrDefault() is MenuItem selectedItem) + { + var navigationService = new NavigationService(); + navigationService.Navigate(selectedItem.PageType); + + // Close the flyout after selection + IsPresented = false; + } + } +} diff --git a/src/Controls/samples/Controls.Sample.Sandbox/Page1.xaml b/src/Controls/samples/Controls.Sample.Sandbox/Page1.xaml new file mode 100644 index 000000000000..5b20c1ae2fb0 --- /dev/null +++ b/src/Controls/samples/Controls.Sample.Sandbox/Page1.xaml @@ -0,0 +1,22 @@ + + + + +