Navigation & dialogs
Navigation moves users through an app while preserving where they came from.
Give FleuryApp a home widget and it creates the root Navigator for you:
void main() => runApp( const FleuryApp(title: 'Projects', home: ProjectsScreen()),);The navigator keeps a stack of screens. Opening a screen adds it to the stack; returning removes it and reveals the screen underneath with its state and focus restored.
| You need to… | Use |
|---|---|
| Open the next screen | context.push(screen) |
| Show a temporary dialog or sheet | context.present(dialog) |
| Close the current route | context.pop() |
| Return a value | context.pop(result) |
| Go back, respecting guards | Navigator.of(context).maybePop() |
Push, present, and pop
Section titled “Push, present, and pop”This first example stays deliberately literal. Each screen labels its stack depth, and each action names the operation it performs. The tabs hold the navigator and its three routes: the home screen, the details screen it pushes, and the dialog that screen presents:
Try the operations in isolation:
- Activate Push details. The home route stays mounted underneath stack depth 2.
- Activate Present dialog, then Confirm and pop. The details screen remains open and reports the dialog result.
- Activate Pop without result. The home screen returns unchanged.
- Push details again and choose Pop with result. The awaiting home screen
now reports
done.
Check context.mounted after awaiting a route: other navigation can remove the
calling screen while the route is open.
Routes stay mounted while covered, so their widget state, scroll positions, and form values remain intact. Each route also gets automatic focus traversal and its own focus memory. You do not wire either behavior screen by screen.
Screens and dialogs
Section titled “Screens and dialogs”Use push for a new step in the user’s journey. A pushed screen fills the
navigator and Esc returns to the screen beneath it:
final result = await context.push<ProjectResult>( const ProjectScreen(),);Routes are widgets, not named route strings: Fleury has no Flutter-style
RouteSettings or named-route table. The navigator stores the screen widget
itself, so screen data travels through its ordinary, typed constructor, and
popUntil<ProjectScreen>() finds a route by its widget type. Otherwise the
type only labels the route in diagnostics and semantics.
Use present for a temporary decision or tool that belongs over the current
screen. The covered screen remains painted, while focus and unhandled keys stay
inside the presented route until it closes:
final confirmed = await context.present<bool>( const DeleteProjectDialog(),);Both methods return a Future<T?>. The route supplies T when it closes:
context.pop(true); // completes the awaiting Future<bool?>An ordinary back or dismissal returns null. Pop with a non-null result only
when the user completed an explicit action.
Dialog placement
Section titled “Dialog placement”Dialogs are centered by default. Use alignment for a sheet or anchored tool;
the frame and padding remain ordinary widgets:
context.present<void>( const CommandSheet(), alignment: Alignment.bottomCenter, barrierColor: Colors.black,);Esc dismisses a presented route; clicking outside it never does. The area
around the dialog only blocks pointer input to the screen behind. Set
barrierDismissible: false when Esc must not dismiss the route either, and
close it explicitly with context.pop() after the required action succeeds.
Choose a placement below. Selecting an option immediately presents the same dialog there; close it to try another position:
Guarding back
Section titled “Guarding back”Wrap a screen in PopScope when Esc or another user-initiated back action
should pause—for example, while an editor has unsaved changes:
When canPop is false, the screen stays open and onBlocked runs. This guard
applies to maybePop, including Fleury’s Esc handling. A programmatic
context.pop() is unconditional, so a successful confirmation can still close
the screen. A Back button in your own UI should therefore call
Navigator.of(context).maybePop(), as the editor’s Back does, so the guard
applies to it too.
In the demo, open the editor and Back immediately: it succeeds because
nothing changed. Open it again, type in the field, and choose Back; the guard now
blocks the request. Save clears the dirty state so the same back action can
close the screen. Reopen the editor and the saved value is still there: the
drafts screen in the drafts_screen.dart tab keeps it and passes it to each
editor it pushes. Discard demonstrates an intentional programmatic pop.
Advanced patterns
Section titled “Advanced patterns”Transitions
Section titled “Transitions”Override the transition for one route when its movement adds useful context:
context.push( const ProjectScreen(), transition: RouteTransition.slide,);Routes fade in and out by default. Fleury includes fade, slide, and none.
To change the default for every route on a navigator, set its transition.
FleuryApp has no transition parameter, so for an app-wide default, give it
your own root navigator as its child:
FleuryApp( title: 'Projects', child: Navigator( transition: RouteTransition.slide, home: const ProjectsScreen(), ),)The built-in presets share one motion path in both directions: pop uses the
flipped forward curve so it retraces the push. A custom RouteTransition can
set reverseCurve when an intentionally different return motion communicates
something useful.
Use the picker to compare all three in both directions. The live example uses the same built-in transition timing as an application:
Replacing and resetting
Section titled “Replacing and resetting”Most navigation is push and pop. These operations handle the stack-changing
cases:
| Operation | Typical use |
|---|---|
pushReplacement(screen) | Replace sign-in with the signed-in screen |
pushAndClear(screen) | Log out and remove the authenticated history |
popUntil<Screen>() | Return to a known screen type |
popToRoot() | Return to this navigator’s first screen |
Nested navigators
Section titled “Nested navigators”FleuryApp(home: ...) inserts the root navigator automatically. Add another
Navigator only when one outer screen needs a self-contained history: a setup
flow, a file browser beside an editor, or a tab with its own back stack.
Follow the files from left to right: app.dart creates the root navigator, which
FleuryApp inserts for you in an app. The projects screen sits on it and pushes
one setup route onto it, the setup flow declares the second navigator, and each
setup step pushes onto that nearest inner navigator. Finishing replaces the one
outer setup route with the project-ready screen in the last tab, removing the
whole inner history together; its Back then returns directly to Projects:
Inside the flow, context.push, context.pop, and Navigator.of(context)
target the nearest navigator. The outer route is unaffected while those inner
steps change. context.rootNavigator is the deliberate escape hatch for
finishing or cancelling the whole flow, or for opening an app-wide route.
API reference
Section titled “API reference”| API | What it does |
|---|---|
FleuryApp(home: screen) | Creates the app’s root navigator |
context.push<T>(screen) | Pushes a full screen and awaits its result |
context.present<T>(screen) | Presents a focus-contained route over the current screen |
context.pop([result]) | Closes the current route, optionally with a result |
Navigator.of(context).maybePop() | Goes back unless a PopScope or a non-dismissible dialog refuses |
Navigator.of(context) | Gets the nearest navigator state |
context.rootNavigator | Gets the app’s root navigator state |
PopScope | Guards user-initiated back attempts |
RouteTransition | Controls route enter and exit animation |
Next steps
Section titled “Next steps”- Focus management explains traversal, focus memory, and presented-route containment in detail.
- Key handling shows how keyboard shortcuts invoke screen-owned actions.
- App entry points covers
home,child, and custom root shells. - The app-shell example
combines app-wide commands, a screen’s
CommandScope, and pushed routes.