| name | nalu-maui-navigation |
| description | Nalu.Maui.Navigation: type-safe MVVM navigation with Shell, lifecycle events, intents, and guards. Use when setting up navigation, passing parameters, preventing navigation (unsaved changes), custom tab bar, or testing navigation in MAUI. |
Nalu.Maui.Navigation
Type-safe navigation on top of Shell: fluent API, scoped disposal, async lifecycle, intents, and guards.
When to use
- Replace Shell string routes with type-based navigation and proper disposal.
- Pass typed data (intents) and get results (awaitable intents).
- Guard navigation (e.g. unsaved changes) and customize tab bar.
Setup
builder.UseNaluNavigation<App>(nav => nav
.AddPage<MainPageModel, MainPage>()
.WithLeakDetectorState(NavigationLeakDetectorState.EnabledWithDebugger)
);
AOT: Use .AddPage<TPageModel, TPage>() (or with interface) per page; .AddPages() is not AOT/trim-compatible.
Page and ViewModel
- Page: Constructor must take the ViewModel (and optionally other deps); set
BindingContext = viewModel.
- ViewModel: Implement
INotifyPropertyChanged (e.g. ObservableObject). Register as Scoped; disposed when removed from stack.
Shell
- Use NaluShell; constructor:
base(navigationService, typeof(DefaultPage)).
- In XAML:
ShellContent with nalu:Navigation.PageType="pages:MainPage".
- In
App: MainPage = new AppShell(navigationService). On Android, cache Window in CreateWindow to avoid duplicate Shell.
Navigation API
- Relative:
Navigation.Relative().Push<DetailPageModel>(), .Pop(), .Pop().Push<NewPageModel>().
- Absolute:
Navigation.Absolute().Root<SettingsPageModel>(), .Root<X>().Add<Y>().
- Execute:
await _navigationService.GoToAsync(navigation).
XAML: NavigateCommand with RelativeNavigation and NavigationPop or NavigationSegment Type="pages:DetailPage".
Lifecycle
Implement only what you need. Order: Entering → animation → Appearing → ... → Disappearing → Leaving → animation → Dispose.
| Interface | When | Fires multiple times? |
|---|
| IEnteringAware | Before animation | No |
| IAppearingAware | After animation | Yes (returning from child) |
| IDisappearingAware | Before leaving | Yes (pushing a child) |
| ILeavingAware | Before removal | No |
| IDisposable | After removal | No |
Keep OnEnteringAsync fast (<30ms); use IAppearingAware for slow work or the Background Loading Pattern.
Intents
- Pass data:
Navigation.Relative().Push<DetailPageModel>().WithIntent(new ContactIntent(42)). Receive in IEnteringAware<ContactIntent> or IAppearingAware<ContactIntent>.
- Get result: Define
class MyIntent : AwaitableIntent<TResult>. Navigate with ResolveIntentAsync<PageModel, TResult>(intent). On pushed page: intent.SetResult(value) then GoToAsync(Navigation.Relative().Pop()).
Use record for intents when possible (value equality for tests).
Guards
Implement ILeavingGuard and CanLeaveAsync(); return false to cancel navigation (e.g. after user confirms "Leave without saving?"). Bypass: Navigation.Relative(NavigationBehavior.IgnoreGuards).Pop().
Custom tab bar
Optional: builder.UseNaluTabBar(). Works with standard Shell and NaluShell.
- NaluTabBar: Use the built-in control and style it (bar/tab colors, shapes, blur). Inherit in XAML and set
NaluTabBar.UseBlurEffect in static constructor if needed.
- Completely custom bar: Use any view as tab bar; on tab press call
NaluTabBar.GoTo(shellSection) (each tab’s BindingContext = corresponding ShellSection). Attach via nalu:NaluShell.TabBarView="{YourCustomTabBar}".
For edge-to-edge and safe area (e.g. content not extending into bottom inset), see Custom TabBarView: edge-to-edge / safe area (root Grid with SafeAreaEdges="None", inner content with SafeAreaEdges="Container").
Full reference: Custom Tab Bar.
Testing
Mock INavigationService (e.g. NSubstitute). Assert with nav.Matches(expectedNav) on the argument passed to GoToAsync. Use record intents for easy equality.
Caveats
- Dispatch navigation from lifecycle events: use
IDispatcher.DispatchAsync(() => _navigationService.GoToAsync(...)) to avoid blocking.
- Match cleanup to creation: Constructor ↔ Dispose, Entering ↔ Leaving, Appearing ↔ Disappearing.
Additional context