diff --git a/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs b/src/Controls/src/Core/FlyoutPage/FlyoutPage.cs index 9645e919a3b5..db3bc6b61c9f 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)); diff --git a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs index 1601f63be7ec..85281cb84f41 100644 --- a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs +++ b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs @@ -85,6 +85,7 @@ void SetupBuilder() handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); + handlers.AddHandler(); #if IOS || MACCATALYST handlers.AddHandler(); @@ -577,6 +578,90 @@ await CreateHandlerAndAddToWindow(window, async () => await AssertionExtensions.WaitForGC([.. references]); } + [Fact("FlyoutPage Detail Does Not Leak When Replaced")] + public async Task FlyoutPageDetailDoesNotLeak() + { + SetupBuilder(); + + var references = new List(); + var flyoutPage = new FlyoutPage + { + Flyout = new ContentPage { Title = "Flyout" }, + Detail = new ContentPage { Title = "Detail" } + }; + + await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => + { + await OnLoadedAsync(flyoutPage); + + var detailPage1 = new ContentPage { Title = "Detail 1" }; + var navPage1 = new NavigationPage(detailPage1); + flyoutPage.Detail = navPage1; + + await OnLoadedAsync(detailPage1); + + references.Add(new(navPage1)); + references.Add(new(navPage1.Handler)); + references.Add(new(navPage1.Handler.PlatformView)); + references.Add(new(detailPage1)); + references.Add(new(detailPage1.Handler)); + references.Add(new(detailPage1.Handler.PlatformView)); + + var detailPage2 = new ContentPage { Title = "Detail 2" }; + var navPage2 = new NavigationPage(detailPage2); + flyoutPage.Detail = navPage2; + + await OnLoadedAsync(detailPage2); + + navPage1 = null; + detailPage1 = null; + }); + + await AssertionExtensions.WaitForGC([.. references]); + } + + [Fact("FlyoutPage Detail Does Not Leak With Multiple Replacements")] + public async Task FlyoutPageDetailDoesNotLeakWithMultipleReplacements() + { + SetupBuilder(); + + var references = new List(); + var flyoutPage = new FlyoutPage + { + Flyout = new ContentPage { Title = "Flyout" }, + Detail = new ContentPage { Title = "Detail" } + }; + + await CreateHandlerAndAddToWindow(new Window(flyoutPage), async () => + { + await OnLoadedAsync(flyoutPage); + + for (int i = 0; i < 5; i++) + { + var detailPage = new ContentPage { Title = $"Detail {i}" }; + var navPage = new NavigationPage(detailPage); + + flyoutPage.Detail = navPage; + await OnLoadedAsync(detailPage); + + if (i < 3) + { + references.Add(new(navPage)); + references.Add(new(navPage.Handler)); + references.Add(new(navPage.Handler.PlatformView)); + references.Add(new(detailPage)); + references.Add(new(detailPage.Handler)); + references.Add(new(detailPage.Handler.PlatformView)); + } + + await Task.Delay(50); + } + + }); + + await AssertionExtensions.WaitForGC([.. references]); + } + [Fact("VisualDiagnosticsOverlay Does Not Leak" #if IOS || MACCATALYST , Skip = "Fails with 'MauiContext should have been set on parent.'" diff --git a/src/Core/src/Platform/Android/Navigation/MauiNavHostFragment.cs b/src/Core/src/Platform/Android/Navigation/MauiNavHostFragment.cs index 29ff73d8e85a..425dabc48bee 100644 --- a/src/Core/src/Platform/Android/Navigation/MauiNavHostFragment.cs +++ b/src/Core/src/Platform/Android/Navigation/MauiNavHostFragment.cs @@ -18,5 +18,11 @@ public MauiNavHostFragment() protected MauiNavHostFragment(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { } + + public override void OnDestroy() + { + base.OnDestroy(); + this.Dispose(); + } } } diff --git a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs index 496990dee1a7..92425541210e 100644 --- a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs +++ b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs @@ -89,8 +89,10 @@ public override void OnDestroy() { _currentView = null; _fragmentContainerView = null; + _navigationManager = null; base.OnDestroy(); + this.Dispose(); } public override Animation OnCreateAnimation(int transit, bool enter, int nextAnim) diff --git a/src/Core/src/Platform/Android/Navigation/ScopedFragment.cs b/src/Core/src/Platform/Android/Navigation/ScopedFragment.cs index 163c6c2d92dd..bce0ac610df7 100644 --- a/src/Core/src/Platform/Android/Navigation/ScopedFragment.cs +++ b/src/Core/src/Platform/Android/Navigation/ScopedFragment.cs @@ -36,6 +36,8 @@ public override void OnDestroy() { base.OnDestroy(); IsDestroyed = true; + + DetailView = null!; } } } diff --git a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs index 546d319a6213..07f4719f77e5 100644 --- a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs +++ b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; + using Android.Content; using Android.OS; using Android.Views; @@ -309,14 +310,45 @@ public virtual void Disconnect() _fragmentContainerView.ViewAttachedToWindow -= OnNavigationPlatformViewAttachedToWindow; _fragmentContainerView.ChildViewAdded -= OnNavigationHostViewAdded; } + + if (_fragmentManager is not null) + { + CleanUpFragments(_fragmentManager); + } _fragmentLifecycleCallbacks?.Disconnect(); _fragmentLifecycleCallbacks = null; - + VirtualView = null; NavigationView = null; SetNavHost(null); _fragmentNavigator = null; + _fragmentManager = null; + _fragmentContainerView = null; + _navGraph = null; + _currentPage = null; + NavigationStack = []; + ActiveRequestedArgs = null; + OnResumeRequestedArgs = null; + } + + + static void CleanUpFragments(FragmentManager fragmentManager) + { + fragmentManager.ExecutePendingTransactionsEx(); + while (fragmentManager.BackStackEntryCount > 0) + { + fragmentManager.PopBackStackImmediate(); + } + + var transaction = fragmentManager.BeginTransactionEx(); + foreach (var fragment in fragmentManager.Fragments) + { + transaction.RemoveEx(fragment); + } + + transaction.CommitNowAllowingStateLoss(); + fragmentManager.ExecutePendingTransactionsEx(); } public virtual void Connect(IView navigationView) @@ -343,7 +375,7 @@ public virtual void Connect(IView navigationView) _fragmentContainerView.ChildViewAdded += OnNavigationHostViewAdded; } } - + void OnNavigationPlatformViewAttachedToWindow(object? sender, AView.ViewAttachedToWindowEventArgs e) { // If the previous Navigation Host Fragment was destroyed then we need to add a new one @@ -505,7 +537,7 @@ void SetNavHost(NavHostFragment? navHost) (FragmentNavigator)NavController .NavigatorProvider .GetNavigator(Java.Lang.Class.FromType(typeof(FragmentNavigator))); - + foreach (var fragment in _navHost.ChildFragmentManager.Fragments) { if (fragment is NavigationViewFragment nvf)