Flutter Widgets Fundamentals

AlertDialog, SnackBar & BottomSheet

50 min Lesson 11 of 18

Dialogs, SnackBars & Bottom Sheets

Flutter provides several built-in widgets for showing temporary messages, confirmation prompts, and action panels. In this lesson you will learn how to use AlertDialog, SimpleDialog, SnackBar, and BottomSheet to communicate with users effectively. These widgets are essential for every production app because they give users feedback, request confirmation, and present contextual actions.

Note: All dialog and sheet functions in Flutter are asynchronous. They return a Future that resolves when the overlay is dismissed. This means you can await the result to know which action the user chose.

showDialog & AlertDialog

The showDialog function displays a modal dialog above the current content. The most common dialog widget is AlertDialog, which provides a title, content area, and action buttons.

Basic AlertDialog

void _showBasicAlert(BuildContext context) {
  showDialog(
    context: context,
    builder: (BuildContext ctx) {
      return AlertDialog(
        title: const Text('Delete Item'),
        content: const Text(
          'Are you sure you want to delete this item? '
          'This action cannot be undone.',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(ctx).pop(false),
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () => Navigator.of(ctx).pop(true),
            child: const Text('Delete'),
          ),
        ],
      );
    },
  );
}

Handling the Dialog Result

Since showDialog returns a Future, you can await the result passed to Navigator.pop.

Awaiting Dialog Result

Future<void> _confirmDelete(BuildContext context) async {
  final bool? confirmed = await showDialog<bool>(
    context: context,
    barrierDismissible: false, // user must tap a button
    builder: (BuildContext ctx) {
      return AlertDialog(
        title: const Text('Confirm Deletion'),
        content: const Text('This will permanently remove the file.'),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(ctx).pop(false),
            child: const Text('Cancel'),
          ),
          FilledButton(
            onPressed: () => Navigator.of(ctx).pop(true),
            style: FilledButton.styleFrom(
              backgroundColor: Colors.red,
            ),
            child: const Text('Delete'),
          ),
        ],
      );
    },
  );

  if (confirmed == true) {
    // Perform the deletion
    debugPrint('Item deleted');
  }
}
Tip: Set barrierDismissible: false when the user must explicitly choose an action. By default the dialog closes when tapping outside it, which could lead to ambiguous results.

Customising AlertDialog Appearance

You can customise the shape, background colour, padding, and elevation of an AlertDialog.

Styled AlertDialog

AlertDialog(
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(16),
  ),
  backgroundColor: Colors.grey.shade50,
  elevation: 8,
  titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
  contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
  actionsPadding: const EdgeInsets.all(16),
  title: const Text('Custom Dialog'),
  content: const Text('This dialog has custom styling.'),
  actions: [
    TextButton(
      onPressed: () => Navigator.pop(context),
      child: const Text('OK'),
    ),
  ],
)

SimpleDialog

When you need the user to choose from a list of options rather than confirm or cancel, use SimpleDialog. Each option is a SimpleDialogOption.

SimpleDialog Example

Future<void> _chooseLanguage(BuildContext context) async {
  final String? selected = await showDialog<String>(
    context: context,
    builder: (BuildContext ctx) {
      return SimpleDialog(
        title: const Text('Choose Language'),
        children: [
          SimpleDialogOption(
            onPressed: () => Navigator.pop(ctx, 'en'),
            child: const Text('English'),
          ),
          SimpleDialogOption(
            onPressed: () => Navigator.pop(ctx, 'ar'),
            child: const Text('Arabic'),
          ),
          SimpleDialogOption(
            onPressed: () => Navigator.pop(ctx, 'es'),
            child: const Text('Spanish'),
          ),
        ],
      );
    },
  );

  if (selected != null) {
    debugPrint('User chose: \$selected');
  }
}

showSnackBar & SnackBar

A SnackBar is a lightweight message bar that appears at the bottom of the screen. It is ideal for brief feedback messages like "Item saved" or "Connection lost". You show a SnackBar through the ScaffoldMessenger.

Basic SnackBar

void _showSnackBar(BuildContext context) {
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Item saved successfully!'),
    ),
  );
}

SnackBar with Action

SnackBars can include an action button, commonly used for undo operations.

SnackBar with Undo Action

void _deleteWithUndo(BuildContext context, String itemName) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text('\$itemName deleted'),
      duration: const Duration(seconds: 5),
      action: SnackBarAction(
        label: 'UNDO',
        onPressed: () {
          // Restore the deleted item
          debugPrint('Undo delete of \$itemName');
        },
      ),
    ),
  );
}

SnackBar Behaviour & Styling

You can control where the SnackBar appears and how it looks.

Floating SnackBar

ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: const Text('This is a floating snackbar'),
    behavior: SnackBarBehavior.floating,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
    margin: const EdgeInsets.all(16),
    backgroundColor: Colors.green.shade700,
    duration: const Duration(seconds: 3),
  ),
);
Note: Use ScaffoldMessenger.of(context) instead of the deprecated Scaffold.of(context).showSnackBar. The ScaffoldMessenger approach works even when the Scaffold is rebuilt and persists across route changes.

showModalBottomSheet

A modal bottom sheet slides up from the bottom of the screen and blocks interaction with the content behind it. It is perfect for presenting a set of actions or a short form.

Basic Modal Bottom Sheet

void _showActions(BuildContext context) {
  showModalBottomSheet(
    context: context,
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(
        top: Radius.circular(20),
      ),
    ),
    builder: (BuildContext ctx) {
      return Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              leading: const Icon(Icons.camera_alt),
              title: const Text('Take Photo'),
              onTap: () {
                Navigator.pop(ctx);
                debugPrint('Camera selected');
              },
            ),
            ListTile(
              leading: const Icon(Icons.photo_library),
              title: const Text('Choose from Gallery'),
              onTap: () {
                Navigator.pop(ctx);
                debugPrint('Gallery selected');
              },
            ),
            ListTile(
              leading: const Icon(Icons.delete),
              title: const Text('Remove Photo'),
              onTap: () {
                Navigator.pop(ctx);
                debugPrint('Remove selected');
              },
            ),
          ],
        ),
      );
    },
  );
}

showBottomSheet (Non-Modal)

Unlike the modal version, showBottomSheet does not block interaction with the rest of the screen. It is called from a Scaffold context.

Persistent Bottom Sheet

void _showPersistentSheet(BuildContext context) {
  Scaffold.of(context).showBottomSheet(
    (BuildContext ctx) {
      return Container(
        padding: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: Colors.blue.shade50,
          borderRadius: const BorderRadius.vertical(
            top: Radius.circular(16),
          ),
        ),
        child: const Text(
          'This is a persistent bottom sheet. '
          'You can still interact with the content behind.',
        ),
      );
    },
  );
}

DraggableScrollableSheet

For bottom sheets that the user can drag to expand or collapse, use DraggableScrollableSheet inside a modal bottom sheet.

Draggable Bottom Sheet

void _showDraggableSheet(BuildContext context) {
  showModalBottomSheet(
    context: context,
    isScrollControlled: true,
    builder: (BuildContext ctx) {
      return DraggableScrollableSheet(
        initialChildSize: 0.4,
        minChildSize: 0.2,
        maxChildSize: 0.9,
        expand: false,
        builder: (BuildContext context, ScrollController controller) {
          return Container(
            decoration: const BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.vertical(
                top: Radius.circular(20),
              ),
            ),
            child: ListView.builder(
              controller: controller,
              itemCount: 30,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text('Item \${index + 1}'),
                );
              },
            ),
          );
        },
      );
    },
  );
}
Tip: Set isScrollControlled: true on the modal bottom sheet when using DraggableScrollableSheet. Without it the sheet height is limited to half the screen.

Practical Example: Confirm Delete Dialog

Combining an AlertDialog with a SnackBar gives a complete user experience for destructive actions.

Complete Delete Flow

class ItemListScreen extends StatefulWidget {
  const ItemListScreen({super.key});

  @override
  State<ItemListScreen> createState() => _ItemListScreenState();
}

class _ItemListScreenState extends State<ItemListScreen> {
  final List<String> _items = [
    'Document A',
    'Document B',
    'Document C',
  ];

  Future<void> _handleDelete(int index) async {
    final confirmed = await showDialog<bool>(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('Delete Document'),
        content: Text(
          'Delete "\${_items[index]}"? This cannot be undone.',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx, false),
            child: const Text('Cancel'),
          ),
          FilledButton(
            onPressed: () => Navigator.pop(ctx, true),
            style: FilledButton.styleFrom(
              backgroundColor: Colors.red,
            ),
            child: const Text('Delete'),
          ),
        ],
      ),
    );

    if (confirmed == true) {
      final removedItem = _items[index];
      setState(() => _items.removeAt(index));

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('\$removedItem deleted'),
            action: SnackBarAction(
              label: 'UNDO',
              onPressed: () {
                setState(() => _items.insert(index, removedItem));
              },
            ),
          ),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('My Documents')),
      body: ListView.builder(
        itemCount: _items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(_items[index]),
            trailing: IconButton(
              icon: const Icon(Icons.delete),
              onPressed: () => _handleDelete(index),
            ),
          );
        },
      ),
    );
  }
}

Practical Example: Action Sheet

A bottom sheet that presents actions like sharing, editing, or deleting a resource.

Action Sheet with Icons

void _showActionSheet(BuildContext context) {
  showModalBottomSheet(
    context: context,
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
    ),
    builder: (ctx) {
      return SafeArea(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Container(
              width: 40,
              height: 4,
              margin: const EdgeInsets.symmetric(vertical: 12),
              decoration: BoxDecoration(
                color: Colors.grey.shade300,
                borderRadius: BorderRadius.circular(2),
              ),
            ),
            ListTile(
              leading: const Icon(Icons.share),
              title: const Text('Share'),
              onTap: () => Navigator.pop(ctx),
            ),
            ListTile(
              leading: const Icon(Icons.edit),
              title: const Text('Edit'),
              onTap: () => Navigator.pop(ctx),
            ),
            ListTile(
              leading: const Icon(Icons.bookmark_add),
              title: const Text('Bookmark'),
              onTap: () => Navigator.pop(ctx),
            ),
            const Divider(),
            ListTile(
              leading: const Icon(Icons.delete, color: Colors.red),
              title: const Text(
                'Delete',
                style: TextStyle(color: Colors.red),
              ),
              onTap: () => Navigator.pop(ctx),
            ),
            const SizedBox(height: 8),
          ],
        ),
      );
    },
  );
}

Summary

  • showDialog + AlertDialog -- modal confirmation or information dialogs
  • SimpleDialog -- choosing from a list of options
  • ScaffoldMessenger.showSnackBar -- brief feedback messages at the bottom
  • showModalBottomSheet -- action sheets and short forms that block background interaction
  • showBottomSheet -- persistent sheets that allow background interaction
  • DraggableScrollableSheet -- expandable sheets the user can resize by dragging
  • All overlay functions return Future values you can await

Practice Exercise

Build a notes app screen with a list of notes. Add a floating action button that opens a bottom sheet with a text field for adding a new note. Each note item should have a delete icon that shows a confirmation AlertDialog. After deletion, show a SnackBar with an undo action that restores the note.