Skip to content
Blog

showDialog Patterns in Flutter: Alerts, Confirmations, and Custom Dialogs

Master showDialog with typed results, barrier dismissal, PopScope, platform-adaptive dialogs, and safe async context handling.

Published on • November 2, 2026

AI Assistant

Confirmation gates and small overlays are everywhere, and showDialog is Flutter’s modal primitive (docs.flutter.dev). The interesting part isn’t opening a dialog — it’s handling its result safely.

Typed Results with await showDialog<T>

showDialog returns a Future<T?> — the value the route was popped with; a barrier tap yields null, so treat == true as the affirmative path:

final confirmed = await showDialog<bool>(
  context: context,
  builder: (ctx) => AlertDialog(
    title: const Text('Discard changes?'),
    content: const Text('Your edits will be lost.'),
    actions: [
      TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')),
      FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Discard')),
    ],
  ),
); // `confirmed == true` is the affirmative path

Variants: AlertDialog, SimpleDialog, Dialog

Pick the lightest surface: AlertDialog for confirmations, SimpleDialog for single-choice pickers, bare Dialog for fully custom layouts:

await showDialog<String>(
  context: context,
  builder: (ctx) => SimpleDialog(title: const Text('Sort by'), children: [
    for (final o in ['Newest', 'Most read', 'A–Z'])
      SimpleDialogOption(onPressed: () => Navigator.pop(ctx, o), child: Text(o)),
  ]),
);

Barrier and Builder Context

Set barrierDismissible: true to let taps outside close the dialog (returns null) — keep it false for destructive confirmations; barrierColor customizes the scrim (default Colors.black54). The builder’s context belongs to the dialog route: use it for Theme.of(ctx) lookups inside the dialog, and the parent context for anything scoped to the host page.

Back Navigation: PopScope Replaces WillPopScope

PopScope( // replaces the deprecated WillPopScope
  canPop: false,
  onPopInvokedWithResult: (didPop, result) { if (!didPop) _showUnsavedWarning(context); },
  child: const DialogForm(),
)

Platform-Adaptive: Cupertino on iOS

final result = Theme.of(context).platform == TargetPlatform.iOS
    ? await showCupertinoDialog<bool>(context: context,
        builder: (ctx) => CupertinoAlertDialog(
            title: const Text('Delete photo'),
            content: const Text('This cannot be undone.'),
            actions: [
              CupertinoDialogAction(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')),
              CupertinoDialogAction(isDestructiveAction: true, onPressed: () => Navigator.pop(ctx, true), child: const Text('Delete')),
            ]))
    : await showDialog<bool>(context: context, builder: (ctx) => AlertDialog(/* same wiring */));

Async Gaps and Small Screens

  • After await showDialog, check context.mounted before ScaffoldMessenger or setState — the host page may be gone.
  • Constrain long content (SingleChildScrollView inside BoxConstraints(maxHeight: ...)) so dialogs don’t overflow compact phones.

Takeaway: await showDialog<T> with Navigator.pop(ctx, result) gives typed outcomes — pair it with context.mounted checks and PopScope for dialogs that survive real navigation.