Skip to content
AB

A Snackbar Is a Lifecycle Problem

Build reliable Flutter snackbars by handling timers, swipe dismissal, navigation, responsive layout, and accessibility.

Ashish BhakhandPublished

Showing a snackbar is easy:

Code
ScaffoldMessenger.of(context).showSnackBar(
  const SnackBar(
    content: Text('Profile saved'),
  ),
);

For most apps, this is the correct solution. ScaffoldMessenger handles presentation, queues messages, and works with Material navigation.

A custom notification becomes useful when the design requires top placement, rich layouts, app-wide overlays, custom animations, or an optional background barrier. That is when lifecycle problems begin.

First choose the right component#

Use the component that matches the message:

MessageBetter component
“Email is required”Inline field error
“Profile saved”Snackbar
“You are offline” for several minutesPersistent banner
“Delete this account?”Dialog

A snackbar disappears. Do not use it for information the user must remember or for a decision that blocks progress.

Prefer ScaffoldMessenger when it is enough#

A practical success message can include an action:

Code
void showArchivedMessage(
  BuildContext context,
  VoidCallback undo,
) {
  final messenger = ScaffoldMessenger.of(context);

  messenger
    ..hideCurrentSnackBar()
    ..showSnackBar(
      SnackBar(
        content: const Text('Conversation archived'),
        action: SnackBarAction(
          label: 'Undo',
          onPressed: undo,
        ),
      ),
    );
}

The hide-and-show policy prevents repeated actions from building a long queue. In another feature, keeping the queue may be better. Decide the behavior based on what users need.

Use the root overlay for app-wide custom notices#

Overlay places a widget above the normal screen content. Resolve it when the BuildContext is valid:

Code
void showTopNotice(BuildContext context, String message) {
  final overlay = Overlay.maybeOf(context, rootOverlay: true);

  if (overlay == null) {
    throw StateError('No Overlay is available');
  }

  late final OverlayEntry entry;

  entry = OverlayEntry(
    builder: (context) => TopNotice(
      message: message,
      onDismissed: entry.remove,
    ),
  );

  overlay.insert(entry);
}

Flutter's Overlay documentation describes how entries are inserted into the overlay stack.

The widget that inserts an entry must also make sure the entry is removed. Otherwise invisible entries can remain mounted.

Prevent the timer and swipe from dismissing twice#

A custom notice may disappear when:

  • its timer ends;
  • the user swipes it;
  • the user taps an action;
  • another notice replaces it.

Two of those events can happen almost together. Keep one dismissal flag:

Code
Timer? dismissTimer;
bool isDismissing = false;

Future<void> dismiss() async {
  if (isDismissing) return;

  isDismissing = true;
  dismissTimer?.cancel();

  await animationController.reverse();
  widget.onDismissed();
}

Every dismissal path should call the same method. The first call starts cleanup; later calls return immediately.

Cancel the timer and dispose the animation controller when the widget is disposed:

Code
@override
void dispose() {
  dismissTimer?.cancel();
  animationController.dispose();
  super.dispose();
}

This small amount of lifecycle code prevents many intermittent snackbar bugs.

Treat a completed swipe differently#

Dismissible has already moved its child away when onDismissed runs. Starting another exit animation at that point only delays removal of an invisible overlay entry.

Code
Dismissible(
  key: const ValueKey('profile-saved-notice'),
  direction: DismissDirection.up,
  onDismissed: (_) => removeImmediately(),
  child: noticeContent,
)

Use an animated dismissal for a timer or close button. Use immediate cleanup after Dismissible has completed its own animation.

Make the notice fit phones and desktops#

A snackbar should not stretch across a wide desktop window. It also should not overflow a narrow phone:

Code
final screenWidth = MediaQuery.sizeOf(context).width;

return SafeArea(
  child: Align(
    alignment: Alignment.topCenter,
    child: Container(
      width: double.infinity,
      constraints: BoxConstraints(
        maxWidth: screenWidth >= 600 ? 560 : double.infinity,
      ),
      margin: const EdgeInsets.all(16),
      child: noticeContent,
    ),
  ),
);

SafeArea keeps the message away from notches and system UI. The maximum width keeps text readable on larger screens.

Announce the message to screen readers#

A transient message appears without receiving focus, so assistive technology may miss it. Mark the content as a live region:

Code
Semantics(
  container: true,
  liveRegion: true,
  label: 'Profile saved successfully',
  child: noticeContent,
)

Flutter exposes this through Semantics liveRegion.

The label should include the useful message, not decorative icon names. If the notice contains an action, make sure the action has a clear semantic label and enough time to be used.

Be careful with blocking backgrounds#

A custom overlay can cover the screen with a tap-blocking barrier:

Code
Stack(
  children: [
    const ModalBarrier(
      dismissible: false,
      color: Color(0x33000000),
    ),
    positionedNotice,
  ],
)

Once background interaction is blocked, the notice behaves more like a modal component. Use this only when continuing to interact with the screen would cause a real problem. A normal success snackbar should not block the app.

Decide what happens when messages arrive together#

Pick one policy:

  • show one message at a time;
  • replace the current message;
  • ignore duplicate messages;
  • stack a small number;
  • prioritize important errors.

For example, a “Saved” message can replace an older “Saved” message. A failed payment should not disappear because a lower-priority analytics notice arrived.

Keep this policy in one notification service rather than making each screen manage its own overlay entries.

Test every dismissal path#

Test timeout and gesture behavior independently:

Code
testWidgets('removes the notice after its duration', (tester) async {
  await tester.pumpWidget(const TestApp());

  showTestNotice();
  await tester.pump();
  expect(find.text('Profile saved'), findsOneWidget);

  await tester.pump(const Duration(seconds: 4));
  await tester.pumpAndSettle();

  expect(find.text('Profile saved'), findsNothing);
});

Also test:

  • swiping just before the timer ends;
  • tapping the action;
  • navigating while the notice is visible;
  • compact and wide screens;
  • safe-area insets;
  • the semantics label;
  • several messages arriving quickly.

Using Fabrik for this#

fabrik_snackbar provides customizable overlay notifications with top or bottom positioning, animations, swipe dismissal, safe-area handling, responsive sizing, and accessibility semantics.

For a standard Material message, keep using ScaffoldMessenger. Reach for a custom solution only when the product requirements need behavior that the standard snackbar does not provide.

← Back to all writing