Skip to content

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 screencontext.push(screen)
Show a temporary dialog or sheetcontext.present(dialog)
Close the current routecontext.pop()
Return a valuecontext.pop(result)
Go back, respecting guardsNavigator.of(context).maybePop()

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:

You write · editable
Widget example() =>
Navigator(transition: RouteTransition.none, home: const _HomeScreen());
Live preview
Live · interactive
click & type to interact ⓘ how this demo runs

Try the operations in isolation:

  1. Activate Push details. The home route stays mounted underneath stack depth 2.
  2. Activate Present dialog, then Confirm and pop. The details screen remains open and reports the dialog result.
  3. Activate Pop without result. The home screen returns unchanged.
  4. 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.

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.

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:

You write · editable
Widget example() =>
Navigator(transition: RouteTransition.none, home: const _DialogPlacement());
Live preview
Live · interactive
click & type to interact ⓘ how this demo runs

Wrap a screen in PopScope when Esc or another user-initiated back action should pause—for example, while an editor has unsaved changes:

You write · editable
Widget example() =>
Navigator(transition: RouteTransition.none, home: const _DraftsScreen());
Live preview
Live · interactive
click & type to interact ⓘ how this demo runs

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.

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:

Live · interactive
click & type to interact ⓘ how this demo runs

Most navigation is push and pop. These operations handle the stack-changing cases:

OperationTypical 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

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:

You write · editable
Widget example() =>
Navigator(transition: RouteTransition.none, home: const _ProjectsScreen());
Live preview
Live · interactive
click & type to interact ⓘ how this demo runs

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.

APIWhat 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.rootNavigatorGets the app’s root navigator state
PopScopeGuards user-initiated back attempts
RouteTransitionControls route enter and exit animation
  • 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.