[net11.0][Android] Implement handler based Shell architecture replacing legacy renderers - #34758
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34758Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34758" |
There was a problem hiding this comment.
Pull request overview
Implements a new handler-based Shell architecture for Android (opt-in via UseAndroidShellHandlers) and refactors shared Android navigation/tab/flyout infrastructure to reduce duplication with existing MAUI handlers/managers.
Changes:
- Added
RuntimeFeature.UseAndroidShellHandlers(defaultfalse) and MSBuild plumbing to enable the feature switch. - Introduced shared Android components/layouts (e.g.,
MauiDrawerLayout,TabbedViewManager,shellitemlayout.axml,shellsectionlayout.axml) and refactoredFlyoutViewHandler/TabbedPageManagerto use them. - Updated Shell compatibility components and test projects to validate the new Android Shell handlers in CI (plus a UI test stabilization tweak).
Reviewed changes
Copilot reviewed 40 out of 40 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Core/src/RuntimeFeature.cs | Adds UseAndroidShellHandlers runtime feature switch. |
| src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt | Public API updates for new tab abstractions/enums. |
| src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt | Public API updates for new tab abstractions/enums. |
| src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt | Public API updates for new tab abstractions/enums. |
| src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt | Public API updates for new tab abstractions/enums. |
| src/Core/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt | Public API updates for new tab abstractions/enums. |
| src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt | Public API updates for new tab abstractions/enums. |
| src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt | Public API updates for new tab abstractions/enums. |
| src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt | Public API updates including MauiDrawerLayout and handler signature changes. |
| src/Core/src/Primitives/TabBarPlacement.cs | Introduces TabBarPlacement enum. |
| src/Core/src/Platform/Android/Resources/values/styles.xml | Adds Android style for Shell TabLayout. |
| src/Core/src/Platform/Android/Resources/Layout/shellsectionlayout.axml | New Android layout used by ShellSection handler. |
| src/Core/src/Platform/Android/Resources/Layout/shellitemlayout.axml | New Android layout used by ShellItem handler. |
| src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs | Adds navigation request queueing and Shell integration for per-tab containers. |
| src/Core/src/Platform/Android/MauiDrawerLayout.cs | New shared DrawerLayout wrapper used by FlyoutView/Shell. |
| src/Core/src/Handlers/FlyoutView/FlyoutViewHandler.Android.cs | Refactors FlyoutViewHandler to use MauiDrawerLayout. |
| src/Core/src/Core/ITabbedView.cs | Expands ITabbedView to support shared tab management surface. |
| src/Core/src/Core/ITab.cs | Adds ITab abstraction for tab items. |
| src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/XFIssue/ShellSearchHandlerItemSizing.cs | Stabilizes screenshot capture via retry/tolerance. |
| src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj | Enables UseAndroidShellHandlers for UI test host app. |
| src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj | Enables UseAndroidShellHandlers for device tests. |
| src/Controls/src/Core/TabbedPage/TabbedPage.cs | Implements expanded ITabbedView surface on TabbedPage. |
| src/Controls/src/Core/Shell/ShellContent.cs | Propagates title updates to support handler-based tab title refresh. |
| src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt | Adds public handler types/methods for Android Shell handlers. |
| src/Controls/src/Core/Platform/Android/TabbedViewManager.cs | New shared manager for ViewPager2 + BottomNavigationView/TabLayout behavior. |
| src/Controls/src/Core/Platform/Android/TabbedPageManager.cs | Refactors TabbedPageManager to delegate tab UI logic to TabbedViewManager. |
| src/Controls/src/Core/Platform/Android/ITabbedViewSource.cs | Adds internal adapter interface to supply tab data without IView. |
| src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs | Conditionally registers Android Shell handlers based on runtime feature. |
| src/Controls/src/Core/Handlers/Shell/ShellTabbedViewAdapters.Android.cs | Adds Shell adapters bridging ShellItem/ShellSection to tab source model. |
| src/Controls/src/Core/Handlers/Shell/ShellHandler.Tizen.cs | Adds stub mappers to satisfy shared mapper entries. |
| src/Controls/src/Core/Handlers/Shell/ShellHandler.cs | Extends property mapper for Android/Tizen/Windows handler scenarios. |
| src/Controls/src/Core/Handlers/Shell/ShellHandler.Android.cs | New Android ShellHandler implementation built on MauiDrawerLayout. |
| src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs | Compatibility updates for toolbar/search behavior and back icon progress handling. |
| src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarAppearanceTracker.cs | Adds null-guard in SetAppearance. |
| src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSearchViewAdapter.cs | Adds JNI ctor + null guard for filter publish. |
| src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutTemplatedContentRenderer.cs | Avoids double-updates when running under new handler path; exposes update methods. |
| src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRecyclerAdapter.cs | Adds additional null/dispose safety. |
| src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets | Maps MSBuild property to runtime feature switch. |
b6433e8 to
0c8fa75
Compare
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
99080f6 to
603ddbc
Compare
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
c7bc2c4 to
d695579
Compare
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
3b5baeb to
7265fdf
Compare
|
/azp run maui-pr-uitests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
edc031f to
74d7ac7
Compare
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
This comment has been minimized.
This comment has been minimized.
Tests Failure Analysis
Test Failure Review: Needs human investigation - click to expandOverall verdict: Needs human investigation The base branch (
Recommended actionInvestigate the MacCatalyst Shell 180-minute timeout by checking whether the cross-platform Shell code changes ( Evidence detailsPR scope: 50 changed files; Android and cross-platform Shell handler code; areas: Shell, Handler, Navigation, Layout. Changed test files include 6 new Android snapshots (ActionModeMenuShouldNotBeVisibleAfterSwitchingTab, BackButtonBehavior_IsVisible_False_ProgrammaticNavStillWorks, ShouldUpdateSearchViewOnPageNavigation, TabBarShouldBeVisibleAfterNavigatingFromModalViaGoToAsync, VerifyShellFlyout_FlyoutIcon, VerifyShellMenuItemsAlignedInRTL) and 3 test case C# files. Cross-platform changes: Build 1477569 (maui-pr): Build results. Failed timeline records: Build 1477582 (maui-pr-uitests): Build results. 30 failed/cancelled timeline records. Recent base-branch builds: 1477478 (failed), 1476416 (failed), 1476158 (failed), 1475414 (canceled), 1473225 (failed) — all on MacCatalyst Shell cancellation: Job exceeded 180-minute limit on agent Limitations: No AzDO bearer token was available; authenticated |
kubaflo
left a comment
There was a problem hiding this comment.
Looks like there are some test failures
Both were found by running the gatherer against a live PR (#34758): 1. Crash: the @() array-subexpression operator on a List[object] throws ArgumentException ("Argument types do not match") from PowerShell's PSToObjectArrayBinder/MaybeDebase for certain element shapes, aborting the run under $ErrorActionPreference=Stop so NO gate is produced at all. The three gate List materializations (pending/failing checks, unexplained legs) now use .ToArray() -- a direct CLR call that bypasses the dynamic binder -- instead of @(). 2. Cross-build contamination / latent false-green vector: the _apis/test/runs list endpoint SILENTLY IGNORES the buildIds filter and returns project-wide runs from the beginning of time (a maui build's query returned 2022-era Roslyn/runtime crossgen runs with build.id 602). Those phantom runs report zero failures, so their failedTests sum to 0 and could falsely confirm a clean device-test build (deviceTestFailedConfirmedZero) over the REAL build that failed. Scope the query by buildUri=vstfs:///Build/Build/<id> (honored server-side) and drop any run carrying an explicit mismatched build id (defense in depth). Verified end-to-end against PR #34758: the gatherer now completes and the test-run set contains only that build's runs (0 cross-repo contaminants, was 20). Harness extended to 167 assertions (PROD-1/PROD-2), all green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kubaflo, the failures on iOS and Mac are unrelated to this PR. |
<!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Regression Details Shell.TitleView is not centered on Windows due to the mapper execution order change in ShellHandler in PR (#34758) ### Root Cause: ShellHandler executes mapper entries in registration order. After a recent change, MapCurrentItem began running before MapToolbar, causing navigation and layout to occur before the toolbar and TitleView were initialized. Previously, MapToolbar executed first, ensuring the CommandBar was fully initialized before layout occurred. This allowed TitleViewManager.UpdateTitleViewWidth() to use a valid CommandBar.ActualWidth and correctly center the TitleView. With the updated order, MapCurrentItem triggers navigation and layout before MapToolbar runs. As a result, CommandBar.ActualWidth is not yet valid during the initial width calculation, causing the TitleView width to be computed incorrectly and appear left-aligned instead of centered. ### Description of Change: Restore the execution order so that MapToolbar runs before MapCurrentItem. This ensures the MauiToolbar and TitleView are fully initialized before navigation triggers layout, allowing TitleViewManager.UpdateTitleViewWidth() to calculate the correct width and center the Shell.TitleView as expected. ### Issues Fixed: Fixes #36322 ### Tested the behaviour in the following platforms - [ ] Android - [x] Windows - [ ] iOS - [ ] Mac ### Output Screenshot Before Issue Fix | After Issue Fix | |----------|----------| |<image width="400" height="200" alt="Before Fix" src="https://github.com/user-attachments/assets/f54df19f-993b-4c56-b47b-823f85cf0b9a">|<image width="400" height="200" alt="After Fix" src="https://github.com/user-attachments/assets/ad043a05-8bdf-4906-8335-8db545d5edb7">|
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!
Description
This PR introduces handler-based Shell architecture for Android, replacing the legacy renderer-based approach (
ShellRenderer,ShellFlyoutRenderer,ShellItemRenderer,ShellSectionRenderer) with new handler classes that reuse standard MAUI platform components. The new architecture follows the same patterns used byFlyoutViewHandler,NavigationViewHandler, andTabbedPageManager, achieving the long-standing architectural goal of building Shell from the same building blocks as non-Shell features.Platform: Android only. iOS/MacCatalyst continue to use legacy
ShellRenderer.Motivation
The legacy Shell renderers on Android are monolithic, tightly coupled, and duplicate functionality that already exists in standard MAUI handlers:
ShellFlyoutRendererextendsDrawerLayoutdirectly (IS-A relationship) — duplicatesFlyoutViewHandlerdrawer logicShellItemRenderermanages its ownBottomNavigationView— duplicatesTabbedPageManagertab logicShellSectionRenderermanages its own toolbar, navigation stack, and content tabs — duplicatesNavigationViewHandlerandTabbedPageManagerpatternsThe new handler architecture:
MauiDrawerLayout— same shared component asFlyoutViewHandlerTabbedViewManager— shared tab management for both Shell andTabbedPagenavigationlayout.axml,shellitemlayout.axml,shellsectionlayout.axml)Shell → ShellHandler,ShellItem → ShellItemHandler,ShellSection → ShellSectionHandlerArchitecture Overview
Handler Hierarchy
Key Design Decisions
Customization Surface
The handler architecture preserves the full customization surface from the legacy renderer path. All factory methods, virtual hooks, and shared classes are accessible to developers.
Protected Virtual Methods (New Handler Hooks)
ShellItemHandlerOnTabReselected(ShellSection)ShellItemHandlerOnSectionChanged(ShellSection, bool)ShellItemHandlerCreateMoreBottomSheet(Action<int, BottomSheetDialog>, List<...>)ShellSectionHandlerOnCreateNavigationAnimation(Context, bool, bool)ShellStackNavigationManager→StackNavigationManager.OnCreateNavigationAnimation()(base ispublic virtual, also benefitsNavigationPage)ShellHandlerIShellContextfactory methodsprotected virtualmethods:CreateTrackerForToolbar(),CreateTabLayoutAppearanceTracker(),CreateBottomNavViewAppearanceTracker(),CreateToolbarAppearanceTracker(),CreateShellFlyoutContentRenderer(),CreateShellFlyoutRenderer(),CreateFragmentForPage(),CreateShellItemTransition()Shared Classes (Not Internalized)
ShellToolbarTrackerpublic class— used by both renderer and handler pathsprotected virtualmethods (navigation icons, search, toolbar items, back button)ShellFlyoutRecyclerAdapterGenerateItemList()—protected virtualIShellContextfactoriesPublic Fragment Classes
All wrapper fragments are top-level
publicclasses in their own files, following the one-class-per-file convention used by the legacy Shell classes. Developers can subclass them to overrideOnCreateView,OnResume,OnViewCreated, etc.ShellItemWrapperFragmentShellItemWrapperFragment.Android.csShellSectionWrapperFragmentShellSectionWrapperFragment.Android.csShellContentNavigationFragmentShellContentNavigationFragment.Android.csFragment Architecture
Shell handlers use wrapper fragments to integrate with Android's
FragmentManager:All fragment classes are public and in their own files. All have default constructors for Android's
Fragment.instantiate()reflection requirement, with null guards inOnCreateView()for graceful restoration handling.New Files
Core Layer (
src/Core/)Controls Layer (
src/Controls/)protected virtualfactory methods.protected virtualhooks:OnTabReselected,OnSectionChanged,CreateMoreBottomSheet.protected virtualhook:OnCreateNavigationAnimation.Modified Files
Handler Registration
ShellHandler,ShellItemHandler,ShellSectionHandler) are now always registered — same as Windows and Tizen. No feature switch needed.Handler as Default (No Feature Switch)
Shell handlers are always registered on Android. No
RuntimeFeatureswitch or MSBuild property is needed — this follows the standard MAUI handler pattern where handlers are the default and legacy renderers are opt-in via explicit registration.Shared Component Extraction
public virtual OnCreateNavigationAnimation()for custom page transitions.Core Interfaces
Compatibility Layer Fixes
protected virtualmethods preserved.Virtual View Changes
PublicAPI
Updated
PublicAPI.Unshipped.txtfiles across all TFMs for new public types and interface members:MauiDrawerLayout,MauiDrawerLayout.FlyoutLayoutModeITab,ITabbedViewexpanded membersTabBarPlacementenumShellHandler,ShellItemHandler,ShellSectionHandlerhandler typesShellItemWrapperFragment,ShellSectionWrapperFragment,ShellContentNavigationFragmentfragment classesOnTabReselected,OnSectionChanged,CreateMoreBottomSheet,OnCreateNavigationAnimationShared Infrastructure Summary
The PR achieves the core architectural goal — Shell now uses the same building blocks as non-Shell features:
Handler as Default
Shell handlers are now the default on Android. The legacy renderer (
ShellRenderer) is still available for apps that need it — register it explicitly inMauiProgram.cs.How to Opt Out (Use Legacy Renderer)
This follows the standard MAUI pattern — handler registration via
AddHandleralways wins (last registration takes precedence). No MSBuild property or feature switch is needed.What Changes By Default
Testing
Controls.TestCases.HostApp.csprojuses the default handler path — all UI tests run against the handler architecture.Controls.DeviceTests.csprojexplicitly registersShellRendererto continue using the legacy renderer path. Shell device tests are tightly coupled to renderer-specific internals (fragment structure, view hierarchy assertions). Migrating them to the handler path is planned as a separate follow-up PR to keep this PR focused on the handler architecture itself.Migration Guidance For Existing Custom Shell Renderers
This section is for apps that subclass legacy Android Shell renderers. Since handlers are now the default, these apps have two options:
ShellRendererexplicitly (see "How to Opt Out" above)Old -> New Mapping
ShellRenderersubclassShellHandlersubclassShellItemRenderersubclassShellItemHandlersubclassShellSectionRenderersubclassShellSectionHandlersubclassCreateTrackerForToolbar()ShellHandler.CreateTrackerForToolbar()(protected virtual)CreateBottomNavViewAppearanceTracker()ShellHandler.CreateBottomNavViewAppearanceTracker()(protected virtual)CreateTabLayoutAppearanceTracker()ShellHandler.CreateTabLayoutAppearanceTracker()(protected virtual)CreateToolbarAppearanceTracker()ShellHandler.CreateToolbarAppearanceTracker()(protected virtual)CreateShellItemRenderer()ShellHandler.CreateShellItemRenderer()(protected virtual)CreateShellSectionRenderer()ShellHandler.CreateShellSectionRenderer()(protected virtual)CreateFragmentForPage()ShellHandler.CreateFragmentForPage()(protected virtual)ShellItemRenderer.OnTabReselected()ShellItemHandler.OnTabReselected()(protected virtual)ShellItemRenderer"More" overflow customizationShellItemHandler.CreateMoreBottomSheet()(protected virtual)Adapter Wrapping Pattern (Important)
In the handler architecture,
CreateShellItemRenderer()/CreateShellSectionRenderer()still return compatibility interfaces, but runtime behavior is bridged through handler adapters.Animation API Translation
SetupAnimation(ShellNavigationSource, FragmentTransaction, Page)OnCreateNavigationAnimation(Context, bool isPopping, bool enter)What changed:
Android.Views.Animations.Animation?instead of mutatingFragmentTransaction.FragmentTransactioncustomization is not exposed on this hook.ShellNavigationSourcedoes not map 1:1; push/pop intent maps toisPopping+enter.Issues Fixed
Fixes #32985