Dart Tutorial

Flutter Lesson 25 of 83 3 min read

Dialogs, SnackBars and Bottom Sheets in Flutter

Show feedback in Flutter with AlertDialog, SnackBar and modal bottom sheets. Learn how to return values and when to use each one.

On this page

Apps need to tell users what happened and ask them to decide things. Flutter has three standard tools, from least to most intrusive.

ToolInterrupts?Use for
SnackBarNoBrief confirmation: “Saved”, “Message sent”
Bottom sheetPartlyA set of options, or a short task
DialogYesA decision the user must make now

SnackBar #

ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: const Text('Item removed from cart'),
    action: SnackBarAction(label: 'Undo', onPressed: _restoreItem),
  ),
);

It disappears on its own after a few seconds. An “Undo” action is often better than a confirmation dialog: the user is not interrupted, and a mistake is still recoverable.

AlertDialog #

showDialog returns a Future that completes with whatever you pass to Navigator.pop when the dialog closes.

import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: DemoPage()));

class DemoPage extends StatelessWidget {
  const DemoPage({super.key});

  Future<void> _confirmDelete(BuildContext context) async {
    final confirmed = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Delete this photo?'),
        content: const Text('This cannot be undone.'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: const Text('Cancel'),
          ),
          FilledButton(
            onPressed: () => Navigator.pop(context, true),
            child: const Text('Delete'),
          ),
        ],
      ),
    );

    if (!context.mounted) return;
    if (confirmed == true) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Photo deleted')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: FilledButton(
          onPressed: () => _confirmDelete(context),
          child: const Text('Delete photo'),
        ),
      ),
    );
  }
}

Points to notice:

  • The result is bool?. It is null if the user taps outside the dialog or presses back.
  • After await, check context.mounted before using context again. The screen may have closed in the meantime.
  • Set barrierDismissible: false to force a choice.
Future<void> _showOptions(BuildContext context) async {
  final choice = await showModalBottomSheet<String>(
    context: context,
    showDragHandle: true,
    builder: (context) => SafeArea(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ListTile(
            leading: const Icon(Icons.photo_camera),
            title: const Text('Take a photo'),
            onTap: () => Navigator.pop(context, 'camera'),
          ),
          ListTile(
            leading: const Icon(Icons.photo_library),
            title: const Text('Choose from gallery'),
            onTap: () => Navigator.pop(context, 'gallery'),
          ),
        ],
      ),
    ),
  );
  debugPrint('Chose: $choice');
}

For a sheet that contains a text field or tall content, add isScrollControlled: true and pad the content by the keyboard height:

showModalBottomSheet<void>(
  context: context,
  isScrollControlled: true,
  builder: (context) => Padding(
    padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom),
    child: const AddNoteForm(),
  ),
);

Other dialogs #

// A list of choices
showDialog<String>(
  context: context,
  builder: (context) => SimpleDialog(
    title: const Text('Sort by'),
    children: [
      SimpleDialogOption(onPressed: () => Navigator.pop(context, 'price'), child: const Text('Price')),
      SimpleDialogOption(onPressed: () => Navigator.pop(context, 'name'), child: const Text('Name')),
    ],
  ),
);

// A dialog styled for the platform (Material on Android, Cupertino on iOS)
showAdaptiveDialog<void>(
  context: context,
  builder: (context) => AlertDialog.adaptive(
    title: const Text('Log out?'),
    actions: [
      TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
      TextButton(onPressed: () => Navigator.pop(context), child: const Text('Log out')),
    ],
  ),
);

For a fully custom layout, return a Dialog(child: ...) from the builder.

A loading dialog #

showDialog<void>(
  context: context,
  barrierDismissible: false,
  builder: (context) => const Center(child: CircularProgressIndicator()),
);
await _upload();
if (context.mounted) Navigator.pop(context); // close it

Often a spinner inside the button or page is kinder than blocking the whole screen.

Good habits #

  • Use a dialog only when the user must decide before continuing. Each one is an interruption.
  • Label buttons with the action: “Delete” and “Cancel”, not “Yes” and “No”.
  • Put the safe choice first and the main action last.
  • Keep SnackBar text to one short line.

Try it yourself #

Add a “Log out” button to a page. Tapping it opens a confirmation dialog. If confirmed, show a SnackBar that says “Logged out” with an “Undo” action.

Practise in the playground Updated by Santosh Adhikari