In Flutter, what changes when you push a MaterialPageRoute or CupertinoPageRoute with fullscreenDialog: true?
answer
- close instead of back
- iOS: rises from the bottom
- no swipe-back, no predictive preview
- the screen below stays still
- still a full page, not showDialog
basics
~20 sThe route becomes a full-screen modal task: Material app bars show a close button, Cupertino nav bars a Cancel button, iOS slides it up from the bottom, the screen below stays still, and pop gestures are disabled.
solid answer
~40 s`fullscreenDialog` is a `PageRoute` flag, default false, for a screen that is a self-contained task, such as composing a message, rather than a step deeper into the app. With it true, an `AppBar` with an implied leading widget shows a close button instead of a back arrow, and a `CupertinoNavigationBar` shows a Cancel button. On iOS the Cupertino builder uses `CupertinoFullscreenDialogTransition`, sliding the page up from the bottom instead of in from the side. The route below does not run its outgoing animation. `PageRoute.popGestureEnabled` returns false, so there is no iOS edge swipe-back and no Android predictive-back preview. The system back button and `Navigator.pop` still close it. It is still a full page route, not a dialog from `showDialog`.
code
dart · 33 linesimport 'package:flutter/cupertino.dart';
Future<void> openComposer(BuildContext context) async {
final String? draft = await Navigator.of(context).push<String>(
CupertinoPageRoute<String>(
fullscreenDialog: true,
builder: (context) => const MessageComposerScreen(),
),
);
if (draft != null) {
debugPrint('Draft saved: $draft');
}
}
class MessageComposerScreen extends StatelessWidget {
const MessageComposerScreen({super.key});
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
// automaticallyImplyLeading shows a Cancel button for a fullscreen dialog.
navigationBar: CupertinoNavigationBar(
middle: const Text('New message'),
trailing: CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () => Navigator.of(context).pop('Hi there'),
child: const Text('Save'),
),
),
child: const SizedBox.expand(),
);
}
}go deeper
Recall the visible effects: close or Cancel button, bottom-up entrance on iOS, and no swipe-back.
Explain why the page below stays still and why popGestureEnabled turns off both platform gestures while back still pops.
Choose which flows are modal tasks, and catch the flag misused on drill-down screens where it breaks iOS navigation.
Set a navigation convention that separates hierarchical pushes from modal tasks across both platforms.
## What the flag means A **route** is one entry on a Navigator's stack. **`fullscreenDialog`** is a boolean on `PageRoute` (so on `MaterialPageRoute`, `CupertinoPageRoute`, `MaterialPage`, `CupertinoPage`, `PageRouteBuilder` and go_router's `CustomTransitionPage`), **false by default**. Setting it to true says: this screen is a **modal task** the user completes or abandons — write a message, edit a profile, report a match — not a step deeper into a hierarchy. The flag does not create a dialog; the route still covers the whole screen and is still a page route. ## What changes | Area | Normal page route | `fullscreenDialog: true` | |---|---|---| | Material `AppBar` implied leading widget | back arrow | close button | | `CupertinoNavigationBar` implied leading widget | back chevron, optionally with the previous title | Cancel button | | iOS transition | slides in from the side with parallax | `CupertinoFullscreenDialogTransition`, up from the bottom | | Route underneath | runs its outgoing transition | stays still | | iOS edge swipe-back | enabled (except on the first route) | disabled | | Android predictive-back preview | available on Android 14+ with the opt-in | disabled | | System back button / `Navigator.pop` | pops | still pops | Some details behind the table: - **The app bar icon** comes from `AppBar` checking `parentRoute?.fullscreenDialog`; you only see it when `automaticallyImplyLeading` is true and you did not pass your own `leading`. - **The Cupertino navigation bar** also skips its hero-style transition between bars for a fullscreen dialog route. - **The route underneath stays still** because both the Material and Cupertino transition mixins refuse to transition to a fullscreen dialog route (`canTransitionTo` returns false) and the dialog refuses to transition from the previous route. - **Gestures stop** because `PageRoute.popGestureEnabled` is `!fullscreenDialog && super.popGestureEnabled`, and both the iOS swipe detector and the Android predictive-back detector check it. - **On Android**, the push itself still uses whatever `PageTransitionsTheme` maps to Android; only the dialog-specific behaviours above change. ## The same flag on every page-route type - **`MaterialPage` and `CupertinoPage`** take `fullscreenDialog` too, for apps that build a `pages` list or use the Router API. - **go_router's `CustomTransitionPage`** has a `fullscreenDialog` parameter as well; its transition is still your own `transitionsBuilder`, so the flag changes the app bar, the gestures and the covered route, not the animation. - **`PageRouteBuilder`** behaves the same way: the flag is honoured, but the entrance is whatever your callback draws. Only the Cupertino builder switches to the bottom-up dialog transition by itself. ## A dating-app example The main flow — match list, then chat — uses normal routes, so iOS users swipe back through it. Writing a report about a profile is a separate task that should not be abandoned by an accidental edge swipe: ```dart Navigator.of(context).push( MaterialPageRoute<ReportReason>( fullscreenDialog: true, builder: (context) => const ReportProfileScreen(), ), ); ``` On iOS the report screen rises from the bottom with a close button; the chat stays in place underneath. The user leaves through the close button or a Submit action that pops with a result. ## When to use it 1. **Creation and editing tasks** — new message, new event, edit profile — where the platform convention is a bottom-up modal with an explicit close. 2. **Flows that should not be dismissed by a stray swipe**, while still leaving the system back button working on Android. 3. **Not** for small confirmations or pickers: those are dialogs or bottom sheets, which are separate APIs. ## Common confusions - It is **not** `showDialog`; a fullscreen dialog route is opaque, full-screen and has no barrier. - It does **not** block the Android back button; blocking back is `PopScope`'s job. - It does **not** change the route's result type or how you await it; `Navigator.pop(context, value)` still completes the push's `Future`. - Because it disables the swipe, putting it on ordinary drill-down screens makes iOS navigation feel broken.
- How is a fullscreenDialog route different from a dialog opened with showDialog?A fullscreen dialog route is an opaque page route that replaces the whole screen and sits in the page-transition system. `showDialog` pushes a separate, non-opaque popup route with a barrier over the current screen. The flag only changes how a page route looks and behaves; it does not turn it into a popup.
- Does a fullscreenDialog route stop the Android system back button?No. It turns off the pop gestures (iOS swipe-back and the predictive-back preview), but a system back still asks the Navigator to pop the top route, and it pops. To block or confirm leaving, the route needs a `PopScope`.
saying these in an interview costs you the question
- fullscreenDialog turns the page into a popup with a barrier.
- fullscreenDialog blocks the Android back button.
- It only swaps the app bar icon and changes nothing else.
- Every pushed screen should use it to stop accidental swipes.
- A fullscreenDialog route cannot return a result to Navigator.push.