Search code examples
flutterdartappbarsnackbarscaffold

Open SnackBar inside function sent to AppBar


I have a page with a form. Once the user clicks on Save, it should display a SnackBar. The save button is in a separate custom AppBar widget (in a separate file) and it has 2 functions that is sent from the page with the form. The AppBar is separated for reusable purposes.

I have tried to use the Builder method but it doesn't work. Then I used the Global Key method and it won't give me an error, but still no SnackBar.

import 'package:flutter/material.dart';

import '../models/author.dart';
import '../widgets/edit_app_bar.dart';

class AuthorEditPage extends StatefulWidget {
  static const PAGE_TITLE = 'Edit Author';
  static const ROUTE_NAME = '/author-edit';

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

class _AuthorEditPageState extends State<AuthorEditPage> {
  @override
  Widget build(BuildContext context) {
    final _operation = ModalRoute.of(context).settings.arguments as String;
    final GlobalKey<ScaffoldState> _scaffoldKey =
        new GlobalKey<ScaffoldState>();

    Author _initialize() {
      return Author(
        id: null,
        name: 'Test',
        nameOther: null,
        facebook: null,
        website: null,
        lastUpdated: DateTime.now().toIso8601String(),
      );
    }

    void _displaySnackBar(Author author) {
      _scaffoldKey.currentState.showSnackBar(
        SnackBar(
          content: Text(
            'Author: ${author.name} added',
          ),
        ),
      );
    }

    return Scaffold(
      key: _scaffoldKey,
      appBar: EditAppBar(
        title: AuthorEditPage.PAGE_TITLE,
        saveAndAddNew: () async {
          Author author = _initialize();
          bool result = await author.createUpdateDelete(_operation);
          if (result) {
            _displaySnackBar(author);
            setState(() {});
          }
        },
        save: () async {
          Author author = _initialize();
          bool result = await author.createUpdateDelete(_operation);
          if (result) {
            _displaySnackBar(author);
            Navigator.of(context).pop();
          }
        },
      ),
      body: null,
    );
  }
}

The Custom AppBar

import 'package:flutter/material.dart';

class EditAppBar extends StatelessWidget with PreferredSizeWidget {
  EditAppBar({
    Key key,
    @required this.title,
    @required this.saveAndAddNew,
    @required this.save,
  }) : super(key: key);

  final String title;
  final Function saveAndAddNew;
  final Function save;

  @override
  Widget build(BuildContext context) {
    return AppBar(
      title: Text(
        title,
      ),
      actions: <Widget>[
        IconButton(
          icon: Icon(
            Icons.add,
          ),
          onPressed: saveAndAddNew,
        ),
        IconButton(
          icon: Icon(
            Icons.save,
          ),
          onPressed: save,
        ),
      ],
    );
  }

  @override
  Size get preferredSize => Size.fromHeight(kToolbarHeight);
}

Any help/guidance is highly appreciated!


Solution

  • Once the user clicks on Save, it should display a SnackBar

    but your save method dispose of the scaffold and return to the previous page, so it doesn't show a snackbar (The Scaffold is not mounted in the next frame)

    save: () async {
      Author author = _initialize();
      bool result = await author.createUpdateDelete(_operation);
      if (result) {
        _displaySnackBar(author);
        Navigator.of(context).pop(); //disposing the Scaffold widget 
      }
    },
    

    If you really want to show a Scaffold you should use the GlobalKey of the ScaffoldWidget you want it to display (in this case of the previous page). Also avoid creating the GlobalKey in the build method, it will create a new one each time you call setState. Here is an example with 2 pages and 2 GloabalKeys, the first page give the second one the globalKey so it can use it if its necesary.

    class Page1 extends StatelessWidget{
      final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
      
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          key: _scaffoldKey,
          body: Center(
           child: FlatButton(
             child: Text('SecondPage'),
             onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) => AuthorEditPage(_scaffoldKey)))
           )
          )
        );
        
      }
      
    }
    
    class AuthorEditPage extends StatefulWidget {
      static const PAGE_TITLE = 'Edit Author';
      static const ROUTE_NAME = '/author-edit';
      final GlobalKey<ScaffoldState> previousScaffold;
      
      AuthorEditPage(this.previousScaffold);
    
      @override
      _AuthorEditPageState createState() => _AuthorEditPageState();
    }
    
    class _AuthorEditPageState extends State<AuthorEditPage> {
      final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); //initialize variables here
      
      Author _initialize() {
          return Author(
            id: null,
            name: 'Test',
            nameOther: null,
            facebook: null,
            website: null,
            lastUpdated: DateTime.now().toIso8601String(),
          );
        }
      
      //give the GlobalKey of the scaffold you want to display the snackbar
      void _displaySnackBar(Author author, GlobalKey<ScaffoldState> scaffold) {
          scaffold?.currentState?.showSnackBar(
            SnackBar(
              content: Text(
                'Author: ${author.name} added',
              ),
            ),
          );
        }
      
      @override
      Widget build(BuildContext context) {
      final _operation = ModalRoute.of(context).settings.arguments as String; //This one requires the context so it's fine to be here
    
      //Avoid creating objects or methods not related to the build method here, you can make them in the class
        return Scaffold(
          key: _scaffoldKey,
          appBar: EditAppBar(
            title: AuthorEditPage.PAGE_TITLE,
            saveAndAddNew: () async {
              Author author = _initialize();
              bool result = await author.createUpdateDelete(_operation);
              print(result);
              if (result) {
                _displaySnackBar(author, _scaffoldKey);
                setState(() {});
              }
            },
            save: () async {
              Author author = _initialize();
              bool result = await author.createUpdateDelete(_operation);
              print(result);
              if (result) {
                _displaySnackBar(author, widget.previousScaffold); //I sue the globalKey of the scaffold of the first page
                Navigator.of(context).pop();
              }
            },
          ),
          body: null,
        );
      }
    }
    

    Flutter 2.0 Stable

    The previous logic is not needed anymore after the release of ScaffoldMessenger.of(context) in the stable version 2.0

    class AuthorEditPage extends StatefulWidget {
      static const PAGE_TITLE = 'Edit Author';
      static const ROUTE_NAME = '/author-edit';
      //final GlobalKey<ScaffoldState> previousScaffold; not needed anymore
    
      AuthorEditPage();
    
      @override
      _AuthorEditPageState createState() => _AuthorEditPageState();
    }
    
    class _AuthorEditPageState extends State<AuthorEditPage> {
      //final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); //initialize variables here //not needed anymore
    
      Author _initialize() {
          return Author(
            id: null,
            name: 'Test',
            nameOther: null,
            facebook: null,
            website: null,
            lastUpdated: DateTime.now().toIso8601String(),
          );
        }
    
      @override
      Widget build(BuildContext context) {
      final _operation = ModalRoute.of(context).settings.arguments as String; //This one requires the context so it's fine to be here
    
      //Avoid creating objects or methods not related to the build method here, you can make them in the class
        return Scaffold(
          appBar: Builder( //Wrap it in a builder to get the context of the scaffold you're currently in
            builder (context) {
               return EditAppBar(
            title: AuthorEditPage.PAGE_TITLE,
            saveAndAddNew: () async {
              Author author = _initialize();
              bool result = await author.createUpdateDelete(_operation);
              print(result);
              if (result) {
                ScaffoldMessenger.maybeOf(context)?.showSnackBar(
                  SnackBar(
                    content: Text(
                     'Author: ${author.name} added',
                    ),
                  ),
                );
                setState(() {});
              }
            },
            save: () async {
              Author author = _initialize();
              bool result = await author.createUpdateDelete(_operation);
              print(result);
              if (result) {
                // It should keep the snackbar across pages
                ScaffoldMessenger.maybeOf(context)?.showSnackBar(
                  SnackBar(
                    content: Text(
                     'Author: ${author.name} added',
                    ),
                  ),
                );
                Navigator.of(context).pop();
              }
            },
          );
            }
          ),
          body: null,
        );
      }
    }