skip to content

In Flutter, what changes when you push a MaterialPageRoute or CupertinoPageRoute with fullscreenDialog: true?

level: juniorimportance: should knowfreq 28%

answer

  1. close instead of back
  2. iOS: rises from the bottom
  3. no swipe-back, no predictive preview
  4. the screen below stays still
  5. still a full page, not showDialog

basics

~20 s

The 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 lines
dart
import '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

for a junior

Recall the visible effects: close or Cancel button, bottom-up entrance on iOS, and no swipe-back.

for a middle

Explain why the page below stays still and why popGestureEnabled turns off both platform gestures while back still pops.

for a senior

Choose which flows are modal tasks, and catch the flag misused on drill-down screens where it breaks iOS navigation.

for a principal

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.