'Flutter: How to close SnackBar when navigating away?

I have an issue. I have a "details" page which can display a SnackBar via ScaffoldMessenger. This snackbar does not hide, its duration is set to long time because it's supposed to stay visible for a long time or until it's dismissed by user or by navigating away from the view.

The last part is the one I'm having issue with. In my dispose method I try to call ScaffoldMessenger.of(_scaffoldKey.currentContext!).hideCurrentSnackBar() but this does not work and throws error that it can't access the context. I suspect it's because the context associated with the key is also the same context that is being removed from the widget tree since I'm navigating away from it (by using back button).

I do not want to handle removing of the snackbar in other views. I know I could probably call ScaffoldMessenger.of(context).clearAllSnackBars() every time I would navigate to them but I don't like it for architectural reasons:

  1. The "detail" view owns the snackbar because it's responsible for creating it. It should be also responsible for disposing of it.
  2. In future I might reorganize my views and then I have to remember to clear the snackbar everywhere. The example I gave you is constrained example but imagine there's accessible sidebar leading to many different views. It would mean adding this code to all those views.

So I really want to somehow remove that snackbar when disposing of the DetailPage view. How can I achieve this?

Link to dartpad.dev

import 'package:flutter/material.dart';

void main() {
  runApp(App());
}

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: const HomePage(), routes: {
      '/detail': (_) => const DetailPage(),
    });
  }
}

class HomePage extends StatelessWidget {
  const HomePage({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: SizedBox.expand(
        child: TextButton(
          child: const Text('See detail'),
          onPressed: () {
            Navigator.pushNamed(context, '/detail');
          },
        ),
      ),
    );
  }
}

class DetailPage extends StatefulWidget {
  const DetailPage({
    Key? key,
  }) : super(key: key);

  @override
  _DetailPageState createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance!.addPostFrameCallback((_) {
      ScaffoldMessenger.of(_scaffoldKey.currentContext!).showSnackBar(SnackBar(
        content: const Text('Entered detail page'),
        duration: const Duration(days: 1),
        action: SnackBarAction(
            label: 'Close',
            onPressed: () {
              ScaffoldMessenger.of(_scaffoldKey.currentContext!)
                  .hideCurrentSnackBar();
            }),
      ));
    });
  }

  @override
  void dispose() {
    ScaffoldMessenger.of(_scaffoldKey.currentContext!).hideCurrentSnackBar();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(title: const Text('Detail')),
      body: const SizedBox.expand(
        child: Center(child: Text('Detail page')),
      ),
    );
  }
}


Solution 1:[1]

Use WillPopScope widget to remove the snackbar.

This widget allows async code to run before the view is popped of the navigation stack and the context is still present in the widget tree at that moment. You can get rid of the overriden dispose method this way.

You can see it working in this dartpad or just note the code below:

import 'package:flutter/material.dart';

void main() {
  runApp(App());
}

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: const HomePage(), routes: {
      '/detail': (_) => const DetailPage(),
    });
  }
}

class HomePage extends StatelessWidget {
  const HomePage({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: SizedBox.expand(
        child: TextButton(
          child: const Text('See detail'),
          onPressed: () {
            Navigator.pushNamed(context, '/detail');
          },
        ),
      ),
    );
  }
}

class DetailPage extends StatefulWidget {
  const DetailPage({
    Key? key,
  }) : super(key: key);

  @override
  _DetailPageState createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance!.addPostFrameCallback((_) {
      ScaffoldMessenger.of(_scaffoldKey.currentContext!).showSnackBar(SnackBar(
        content: const Text('Entered detail page'),
        duration: const Duration(days: 1),
        action: SnackBarAction(
            label: 'Close',
            onPressed: () {
              ScaffoldMessenger.of(_scaffoldKey.currentContext!)
                  .hideCurrentSnackBar();
            }),
      ));
    });
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
        child: Scaffold(
          key: _scaffoldKey,
          appBar: AppBar(title: const Text('Detail')),
          body: const SizedBox.expand(
            child: Center(child: Text('Detail page')),
          ),
        ),
        onWillPop: () async {
          ScaffoldMessenger.of(_scaffoldKey.currentContext!).hideCurrentSnackBar();
          return Future.value(true);
        });
  }
}

I think the only downside is that hideCurrentSnackBar does not complete with Future so the animation does not finish. Maybe there'd be a way to do it with some sort of Completer.

Solution 2:[2]

    try{ ScaffoldMessenger.of(context).show()/// show snackbar
}catch(e){
print(e);
}

putting scaffold messenger inside a try catch prevent disposed context usage error

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 user3056783
Solution 2 A.K.J.94