Skip to content

Commit 7265fdf

Browse files
Optimize the shell handler
1 parent 43733c5 commit 7265fdf

3 files changed

Lines changed: 105 additions & 56 deletions

File tree

src/Controls/src/Core/Handlers/Shell/ShellItemHandler.Android.cs

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#nullable enable
22
using System;
33
using System.Collections.Generic;
4-
using System.Threading.Tasks;
54
using Android.OS;
65
using Android.Views;
76
using AndroidX.CoordinatorLayout.Widget;
@@ -298,10 +297,15 @@ internal void SwitchToShellItem(ShellItem newItem)
298297
// That causes SetSelectedTab on the old BNV, firing OnNavigationItemSelected which
299298
// poisons the old ShellItem's CurrentItem via ProposeSection.
300299
_switchingShellItem = true;
301-
SetVirtualView(newItem);
302-
_switchingShellItem = false;
303-
304-
_preserveFragmentResources = false;
300+
try
301+
{
302+
SetVirtualView(newItem);
303+
}
304+
finally
305+
{
306+
_switchingShellItem = false;
307+
_preserveFragmentResources = false;
308+
}
305309

306310
// Rebuild ViewPager2 adapter for new ShellItem's sections
307311
SetupViewPagerAdapter();
@@ -638,37 +642,15 @@ void NotifyTopTabsForSectionSwitch(ShellSection? oldSection, ShellSection? newSe
638642
}
639643

640644
/// <summary>
641-
/// Handles the back button press. Returns true if navigation was handled, false otherwise.
642-
/// Back navigation is delegated to the current section's StackNavigationManager.
645+
/// Handles the back button press by delegating to Shell's full navigation pipeline.
646+
/// Shell.SendBackButtonPressed() handles BackButtonBehavior commands, page overrides,
647+
/// navigation stack pops, modal dismissal, and ShellNavigatingEventArgs cancellation.
648+
/// Returns true if Shell handled it, false if system should handle (app exit).
643649
/// </summary>
644650
internal bool OnBackButtonPressed()
645651
{
646-
if (_shellSection is null)
647-
{
648-
return false;
649-
}
650-
651-
var stack = _shellSection.Stack;
652-
653-
// If we're at the root page, don't handle back - let the system handle it
654-
if (stack.Count <= 1)
655-
{
656-
return false;
657-
}
658-
659-
// We have pages in the stack, so we can pop
660-
Task.Run(async () =>
661-
{
662-
try
663-
{
664-
await _shellSection.Navigation.PopAsync();
665-
}
666-
catch (Exception)
667-
{
668-
}
669-
});
670-
671-
return true; // We handled the back press
652+
var shell = VirtualView?.FindParentOfType<Shell>();
653+
return shell?.SendBackButtonPressed() ?? false;
672654
}
673655

674656
#endregion Navigation Support
@@ -1156,10 +1138,17 @@ public void Dispose()
11561138
/// </summary>
11571139
class ShellItemWrapperFragment : Fragment
11581140
{
1159-
readonly ShellItemHandler _handler;
1141+
readonly ShellItemHandler? _handler;
11601142
CoordinatorLayout? _rootLayout;
11611143
ShellBackPressedCallback? _backPressedCallback;
11621144

1145+
// Default constructor required by Android's FragmentManager for fragment restoration.
1146+
// Without this, FragmentManager.instantiate() crashes on process-death restoration.
1147+
public ShellItemWrapperFragment()
1148+
{
1149+
_handler = null;
1150+
}
1151+
11631152
public ShellItemWrapperFragment(ShellItemHandler handler)
11641153
{
11651154
_handler = handler;
@@ -1169,6 +1158,13 @@ public ShellItemWrapperFragment(ShellItemHandler handler)
11691158

11701159
public override AView OnCreateView(LayoutInflater inflater, ViewGroup? container, Bundle? savedInstanceState)
11711160
{
1161+
// If restored without proper handler reference, return empty view.
1162+
// The Shell infrastructure will recreate proper fragments after reconnecting.
1163+
if (_handler is null)
1164+
{
1165+
return new global::Android.Widget.FrameLayout(inflater.Context!);
1166+
}
1167+
11721168
// Inflate from XML layout — consistent with NavigationViewHandler/FlyoutViewHandler pattern
11731169
var rootView = inflater.Inflate(Resource.Layout.shellitemlayout, container, false)
11741170
?? throw new InvalidOperationException("shellitemlayout inflation failed");
@@ -1195,8 +1191,14 @@ public override void OnViewCreated(AView view, Bundle? savedInstanceState)
11951191
{
11961192
base.OnViewCreated(view, savedInstanceState);
11971193

1194+
// Skip setup if restored without handler (parameterless constructor path)
1195+
if (_handler is null)
1196+
{
1197+
return;
1198+
}
1199+
11981200
// Setup back button handling
1199-
_backPressedCallback = new ShellBackPressedCallback(_handler);
1201+
_backPressedCallback = new ShellBackPressedCallback(_handler, this);
12001202
RequireActivity().OnBackPressedDispatcher.AddCallback(ViewLifecycleOwner, _backPressedCallback);
12011203

12021204
// Setup the shared toolbar
@@ -1255,20 +1257,33 @@ protected override void Dispose(bool disposing)
12551257
sealed class ShellBackPressedCallback : AndroidX.Activity.OnBackPressedCallback
12561258
{
12571259
readonly ShellItemHandler _handler;
1260+
readonly Fragment _fragment;
12581261

1259-
public ShellBackPressedCallback(ShellItemHandler handler) : base(true)
1262+
public ShellBackPressedCallback(ShellItemHandler handler, Fragment fragment) : base(true)
12601263
{
12611264
_handler = handler;
1265+
_fragment = fragment;
12621266
}
12631267

12641268
public override void HandleOnBackPressed()
12651269
{
1266-
// Let the handler try to handle the back press
1270+
// Route through Shell's full back navigation pipeline.
1271+
// Shell.SendBackButtonPressed() handles:
1272+
// - BackButtonBehavior.Command execution
1273+
// - Page.OnBackButtonPressed() overrides
1274+
// - Navigation stack pops (via Shell.OnBackButtonPressed)
1275+
// - Modal stack dismissal
1276+
// - ShellNavigatingEventArgs cancellation
1277+
// This matches the old renderer behavior where the lifecycle chain
1278+
// (Activity → Window → Shell) naturally invoked the full pipeline.
12671279
if (!_handler.OnBackButtonPressed())
12681280
{
1269-
// Handler didn't handle it (we're at root), let system handle it
1281+
// Shell didn't handle it (at root, no stack to pop, not cancelled).
1282+
// Forward to system by temporarily disabling this callback so the
1283+
// dispatcher falls through to the next handler in the chain
1284+
// (e.g., the Activity's default that finishes the app).
12701285
this.Enabled = false;
1271-
// The system will handle app exit
1286+
_fragment.RequireActivity().OnBackPressedDispatcher.OnBackPressed();
12721287
this.Enabled = true;
12731288
}
12741289
}
@@ -1389,7 +1404,7 @@ public override Fragment CreateFragment(int position)
13891404
return observableFragment.Fragment;
13901405
}
13911406

1392-
throw new InvalidOperationException($"ShellSectionRenderer for {section.Title} is not an IShellObservableFragment");
1407+
throw new InvalidOperationException($"ShellSectionHandler for {section.Title} is not an IShellObservableFragment");
13931408
}
13941409

13951410
public IShellSectionRenderer? GetRenderer(int position)

src/Controls/src/Core/Handlers/Shell/ShellSectionHandler.Android.cs

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public partial class ShellSectionHandler : ElementHandler<ShellSection, AView>,
3636
ShellContentFragmentAdapter? _adapter;
3737
TabbedViewManager? _tabbedViewManager;
3838
ShellSectionTabbedViewAdapter? _shellSectionAdapter;
39+
ViewPagerPageChangeCallback? _pageChangedCallback;
3940

4041
/// <summary>
4142
/// Gets the toolbar tracker from the parent ShellItemHandler.
@@ -229,9 +230,9 @@ internal void SetupViewPagerAdapter()
229230
};
230231
_tabbedViewManager.SetElement(_shellSectionAdapter);
231232

232-
// Register page change callback
233-
var pageChangedCallback = new ViewPagerPageChangeCallback(this);
234-
_viewPager.RegisterOnPageChangeCallback(pageChangedCallback);
233+
// Register page change callback (stored in field for cleanup in DisconnectHandler)
234+
_pageChangedCallback = new ViewPagerPageChangeCallback(this);
235+
_viewPager.RegisterOnPageChangeCallback(_pageChangedCallback);
235236

236237
// Update TabLayout visibility based on item count
237238
UpdateTabLayoutVisibility();
@@ -409,6 +410,13 @@ protected override void DisconnectHandler(AView platformView)
409410
_tabbedViewManager = null;
410411
_shellSectionAdapter = null;
411412

413+
// Unregister page change callback
414+
if (_pageChangedCallback is not null && _viewPager is not null)
415+
{
416+
_viewPager.UnregisterOnPageChangeCallback(_pageChangedCallback);
417+
_pageChangedCallback = null;
418+
}
419+
412420
// Cleanup adapter
413421
_adapter = null;
414422
_viewPager?.Adapter = null;
@@ -459,7 +467,7 @@ public static void MapCurrentItem(ShellSectionHandler handler, ShellSection shel
459467

460468
void OnItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
461469
{
462-
if (_adapter is null)
470+
if (_adapter is null || _viewPager is null || _parentFragment is null || VirtualView is null || MauiContext is null)
463471
{
464472
return;
465473
}
@@ -477,7 +485,7 @@ void OnItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e
477485
// Replace with a fresh adapter to avoid stale state restoration
478486
_viewPager?.Adapter = null;
479487

480-
_adapter = new ShellContentFragmentAdapter(VirtualView, _parentFragment!, MauiContext!.MakeScoped(fragmentManager: _parentFragment!.ChildFragmentManager))
488+
_adapter = new ShellContentFragmentAdapter(VirtualView, _parentFragment, MauiContext.MakeScoped(fragmentManager: _parentFragment.ChildFragmentManager))
481489
{
482490
Handler = this
483491
};
@@ -657,6 +665,8 @@ internal class ShellContentFragmentAdapter : FragmentStateAdapter
657665
readonly ShellSection _shellSection;
658666
readonly IMauiContext _mauiContext;
659667
IList<ShellContent>? _visibleItems;
668+
long _itemIdCounter;
669+
readonly Dictionary<ShellContent, long> _contentIds = new Dictionary<ShellContent, long>();
660670
IShellSectionController SectionController => (IShellSectionController)_shellSection;
661671

662672
public ShellContentFragmentAdapter(ShellSection shellSection, Fragment parentFragment, IMauiContext mauiContext)
@@ -673,10 +683,24 @@ public ShellContentFragmentAdapter(ShellSection shellSection, Fragment parentFra
673683

674684
/// <summary>
675685
/// Refreshes the visible items list. Called when items collection changes (add/remove/visibility).
686+
/// Removes stale IDs for items no longer present so ContainsItem returns false,
687+
/// causing FragmentStateAdapter to remove their fragments.
676688
/// </summary>
677689
public void OnItemsCollectionChanged()
678690
{
679-
_visibleItems = SectionController.GetItems();
691+
var newItems = SectionController.GetItems();
692+
693+
// Remove IDs for items that are no longer visible
694+
var toRemove = new List<ShellContent>();
695+
foreach (var kvp in _contentIds)
696+
{
697+
if (!newItems.Contains(kvp.Key))
698+
toRemove.Add(kvp.Key);
699+
}
700+
foreach (var item in toRemove)
701+
_contentIds.Remove(item);
702+
703+
_visibleItems = newItems;
680704
}
681705

682706
public override Fragment CreateFragment(int position)
@@ -690,13 +714,24 @@ public override Fragment CreateFragment(int position)
690714
return new ShellContentNavigationFragment(shellContent, _mauiContext, Handler);
691715
}
692716

717+
/// <summary>
718+
/// Returns a stable ID for each ShellContent. Uses an incrementing counter
719+
/// with a dictionary to avoid hash collisions (GetHashCode is not guaranteed unique).
720+
/// Same pattern as ShellSectionFragmentAdapter.GetItemId.
721+
/// </summary>
693722
public override long GetItemId(int position)
694723
{
695724
if (_visibleItems is null || position >= _visibleItems.Count)
696725
{
697726
return -1;
698727
}
699-
return _visibleItems[position].GetHashCode();
728+
var content = _visibleItems[position];
729+
if (!_contentIds.TryGetValue(content, out var id))
730+
{
731+
id = ++_itemIdCounter;
732+
_contentIds[content] = id;
733+
}
734+
return id;
700735
}
701736

702737
public override bool ContainsItem(long itemId)
@@ -705,12 +740,10 @@ public override bool ContainsItem(long itemId)
705740
{
706741
return false;
707742
}
708-
foreach (var item in _visibleItems)
743+
foreach (var kvp in _contentIds)
709744
{
710-
if (item.GetHashCode() == itemId)
711-
{
745+
if (kvp.Value == itemId && _visibleItems.Contains(kvp.Key))
712746
return true;
713-
}
714747
}
715748
return false;
716749
}
@@ -853,7 +886,7 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup? container
853886
_rootPage = ((IShellContentController)_shellContent)?.GetOrCreateContent();
854887
if (_rootPage is null)
855888
{
856-
return null!;
889+
return new FrameLayout(inflater.Context!);
857890
}
858891

859892
// Subscribe to navigation events EARLY - before anything else

src/Controls/src/Core/Platform/Android/TabbedViewManager.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ internal class TabbedViewManager
3636
{
3737
Fragment _tabLayoutFragment;
3838
ColorStateList _originalTabTextColors;
39-
ColorStateList _orignalTabIconColors;
39+
ColorStateList _originalTabIconColors;
4040
ColorStateList _newTabTextColors;
4141
ColorStateList _newTabIconColors;
4242
FragmentManager _fragmentManager;
@@ -481,6 +481,7 @@ protected virtual void OnTabsCollectionChanged(object sender, NotifyCollectionCh
481481

482482
UpdateTabIcons();
483483
#pragma warning disable CS0618 // Type or member is obsolete
484+
tabs.RemoveOnTabSelectedListener(_listeners);
484485
tabs.AddOnTabSelectedListener(_listeners);
485486
#pragma warning restore CS0618 // Type or member is obsolete
486487
}
@@ -986,17 +987,17 @@ internal virtual ColorStateList GetItemIconTintColorState(int tabIndex)
986987
return null;
987988
}
988989

989-
if (_orignalTabIconColors is null)
990+
if (_originalTabIconColors is null)
990991
{
991-
_orignalTabIconColors = IsBottomTabPlacement ? _bottomNavigationView.ItemIconTintList : _tabLayout.TabIconTint;
992+
_originalTabIconColors = IsBottomTabPlacement ? _bottomNavigationView.ItemIconTintList : _tabLayout.TabIconTint;
992993
}
993994

994995
Color barItemColor = BarItemColor;
995996
Color barSelectedItemColor = BarSelectedItemColor;
996997

997998
if (barItemColor is null && barSelectedItemColor is null)
998999
{
999-
return _orignalTabIconColors;
1000+
return _originalTabIconColors;
10001001
}
10011002

10021003
if (_newTabIconColors is not null)

0 commit comments

Comments
 (0)