Material Design’s Snackbars are a great way to give feedback to users, and even allow them to undo or retry an action. So, how do you display a Snackbar in Flutter?

If you create a Material App in Flutter, you will use a Scaffold. In the Scaffold.of documentation, there are examples to show a Snackbar. While very useful, in all those examples, the Snackbar is shown when the user presses a button. But often, you need to show a Snackbar following an async operation, such as a server call.

In this code tutorial, we will demonstrate how to show a snackbar outside the build method of your widget, and will call this after an async operation.

Setting up the app

We will create a simple app with one screen. The screen has a button, which simulates calling a server API by waiting for 5 seconds. Then, a snackbar saying there was a server error is shown, with a “Retry” button. We will use a basic MVP structure for this screen.

To follow the code tutorial, create a new app as follows.

Create new app flutter create snackbarexample 1 flutter create snackbarexample

If you’re unsure how to set up a Flutter app, check out Getting started with Flutter official tutorial.

Firstly, we create a Material app in main.dart, which will launch the HomePage widget.

main.dart import 'package:flutter/material.dart'; import 'home_page.dart'; void main() { runApp(new MyApp()); } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return new MaterialApp( title: 'Snackbar Example', theme: new ThemeData( primaryColor: const Color(0xFF43a047), accentColor: const Color(0xFFffcc00), primaryColorBrightness: Brightness.dark, ), home: new HomePage(), ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import 'package:flutter/material.dart' ; import 'home_page.dart' ; void main ( ) { runApp ( new MyApp ( ) ) ; } class MyApp extends StatelessWidget { // This widget is the root of your application. @ override Widget build ( BuildContext context ) { return new MaterialApp ( title : 'Snackbar Example' , theme : new ThemeData ( primaryColor : const Color ( 0xFF43a047 ) , accentColor : const Color ( 0xFFffcc00 ) , primaryColorBrightness : Brightness . dark , ) , home : new HomePage ( ) , ) ; } }

Secondly, we create home_page.dart. This displays a button.

home_page.dart import 'package:flutter/material.dart'; class HomePage extends StatefulWidget { HomePage({Key key}) : super(key: key); @override _HomePageState createState() => new _HomePageState(); } class _HomePageState extends State<HomePage> { @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text("Snackbar example"), ), body: new Center( child: new RaisedButton( onPressed: _buttonPressed, child: new Text('SIMULATE SERVER CALL'), ), ), ); } void _buttonPressed() { // TODO } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import 'package:flutter/material.dart' ; class HomePage extends StatefulWidget { HomePage ( { Key key } ) : super ( key : key ) ; @ override _HomePageState createState ( ) = > new _HomePageState ( ) ; } class _HomePageState extends State < HomePage > { @ override void initState ( ) { super . initState ( ) ; } @ override Widget build ( BuildContext context ) { return new Scaffold ( appBar : new AppBar ( title : new Text ( "Snackbar example" ) , ) , body : new Center ( child : new RaisedButton ( onPressed : _buttonPressed , child : new Text ( 'SIMULATE SERVER CALL' ) , ) , ) , ) ; } void _buttonPressed ( ) { // TODO } }

Looking for a Flutter job? Check out my job board dedicated to Flutter at flutterjobs.info

Setting up the MVP structure

I like to use “contracts” when using MVP. Contracts are interfaces (in Java) or abstract classes (in Dart) that define the methods for each component of the feature, ie View, Model, and Presenter.

Note: You do not need to set up contracts for applying MVP, but I find it very helpful to have them. I define them all in one file, and this makes it easy to understand what a feature does. When I worked as an Android DPE at Google, I worked on the first release of the Android Architecture Blueprints project and we decided to use contracts. I have used contracts on all my professional projects since, and I haven’t looked back: this really helps make a complex app maintainable!

So let’s set up our contract for this feature, by creating home_contract.dart.

home_contract.dart abstract class View { } abstract class Model { } abstract class Presenter { } 1 2 3 4 5 6 7 8 9 10 11 abstract class View { } abstract class Model { } abstract class Presenter { }

There are 2 possible user actions: tapping the main button, and tapping the “retry” button in the snackbar. We add them to the Presenter contract. As the server call is async, we set them up as async too, because the Presenter will need to wait for the results of the call before sending back some data to the View.

home_contract.dart/Presenter import 'dart:async'; [...] abstract class Presenter { Future makeServerCall(); Future retryServerCall(); } 1 2 3 4 5 6 7 8 9 10 11 import 'dart:async' ; [ . . . ] abstract class Presenter { Future makeServerCall ( ) ; Future retryServerCall ( ) ; }

For simplicity, both user actions lead to the same model request, which is to simulate calling a server, an async action.

home_contract.dart/Model abstract class Model { Future simulateServerCall(); } 1 2 3 4 5 abstract class Model { Future simulateServerCall ( ) ; }

Finally, in our simplified case, the view has only one method, which is to show the error message (ie the Snackbar), as our simulated server call always fails.

Note: in a real app, you would also have a method to show server call in progress (ie showing a ProgressBar), and, obviously, a method to show the updated data retrieved from the server.

home_contract.dart/View abstract class View { void showErrorMessage(); } 1 2 3 4 5 abstract class View { void showErrorMessage ( ) ; }

Now, we create Model and Presenter classes that implement the abstract classes. The View abstract class is implemented in HomePage.

Firstly, let’s start with home_presenter.dart. The Presenter has a reference to the View and the Model, as it acts as a “coordinator” between View and Model. For our simplified example, the logic is simple: it waits while the model does the simulated server call, then it asks the view to display the error message (ie Snackbar).

home_presenter.dart import 'home_contract.dart'; import 'dart:async'; class HomePresenter implements Presenter { Model _model; View _view; HomePresenter(this._model, this._view); @override Future makeServerCall() async { await _model.simulateServerCall(); _view.showErrorMessage(); } @override Future retryServerCall() async { await _model.simulateServerCall(); _view.showErrorMessage(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import 'home_contract.dart' ; import 'dart:async' ; class HomePresenter implements Presenter { Model _model ; View _view ; HomePresenter ( this . _model , this . _view ) ; @ override Future makeServerCall ( ) async { await _model . simulateServerCall ( ) ; _view . showErrorMessage ( ) ; } @ override Future retryServerCall ( ) async { await _model . simulateServerCall ( ) ; _view . showErrorMessage ( ) ; } }

Secondly, let’s set up home_model.dart.

home_model.dart import 'dart:async'; import 'home_contract.dart'; class HomeModel implements Model { @override Future simulateServerCall() async { // TODO: implement simulateServerCall return new Future.value(null); } } 1 2 3 4 5 6 7 8 9 10 11 import 'dart:async' ; import 'home_contract.dart' ; class HomeModel implements Model { @ override Future simulateServerCall ( ) async { // TODO: implement simulateServerCall return new Future . value ( null ) ; } }

And, lastly, HomePage itself will implement the View, and will create the Presenter when initialised.

home_page.dart [...] import 'home_contract.dart'; import 'home_model.dart'; import 'home_presenter.dart'; [...] class _HomePageState extends State<HomePage> implements View { HomePresenter _presenter; @override void initState() { super.initState(); _presenter = new HomePresenter(new HomeModel(), this); } [...] void _buttonPressed() { _presenter.makeServerCall(); } @override void showErrorMessage() { // TODO: implement showErrorMessage } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 [ . . . ] import 'home_contract.dart' ; import 'home_model.dart' ; import 'home_presenter.dart' ; [ . . . ] class _HomePageState extends State < HomePage > implements View { HomePresenter _presenter ; @ override void initState ( ) { super . initState ( ) ; _presenter = new HomePresenter ( new HomeModel ( ) , this ) ; } [ . . . ] void _buttonPressed ( ) { _presenter . makeServerCall ( ) ; } @ override void showErrorMessage ( ) { // TODO: implement showErrorMessage } }

Adding an async operation

The simulated server call should be async, so let’s implement it as such. For our code tutorial purpose, we will simply create a future from a duration of 5 seconds.

home_model.dart @override Future simulateServerCall() async { return new Future.delayed(new Duration(seconds: 5)); } 1 2 3 4 @ override Future simulateServerCall ( ) async { return new Future . delayed ( new Duration ( seconds : 5 ) ) ; }

Showing a snackbar

To show a snackbar, Flutter provides Scaffold.of(context).showSnackBar . However, the context should be the context of a descendant of a Scaffold, and not the context that includes a Scaffold. Put simply, if we use the context of the HomePage widget, which includes a Scaffold, we will get an error as below.

Error message Scaffold.of() called with a context that does not contain a Scaffold. 1 Scaffold . of ( ) called with a context that does not contain a Scaffold .

So, instead, we need to use a BuildContext for the body of the Scaffold, and store it in a variable, as below.

home_page.dart class _HomePageState extends State<HomePage> implements View { HomePresenter _presenter; BuildContext _scaffoldContext; [...] @override Widget build(BuildContext context) { Widget body = new Center( child: new RaisedButton( onPressed: _buttonPressed, child: new Text('SIMULATE SERVER CALL'), ), ); return new Scaffold( appBar: new AppBar( title: new Text("Snackbar example"), ), body: new Builder( builder: (BuildContext context) { _scaffoldContext = context; return body; } ), ); } [...] @override void showErrorMessage() { Scaffold.of(_scaffoldContext).showSnackBar(new SnackBar( content: new Text('Server error'), duration: new Duration(seconds: 5), )); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class _HomePageState extends State < HomePage > implements View { HomePresenter _presenter ; BuildContext _scaffoldContext ; [ . . . ] @ override Widget build ( BuildContext context ) { Widget body = new Center ( child : new RaisedButton ( onPressed : _buttonPressed , child : new Text ( 'SIMULATE SERVER CALL' ) , ) , ) ; return new Scaffold ( appBar : new AppBar ( title : new Text ( "Snackbar example" ) , ) , body : new Builder ( builder : ( BuildContext context ) { _scaffoldContext = context ; return body ; } ) , ) ; } [ . . . ] @ override void showErrorMessage ( ) { Scaffold . of ( _scaffoldContext ) . showSnackBar ( new SnackBar ( content : new Text ( 'Server error' ) , duration : new Duration ( seconds : 5 ) , ) ) ; } }

Next, we want to show a “Retry” button on the Snackbar and hook it up to our presenter. This is done as below.

home_page.dart @override void showErrorMessage() { Scaffold.of(_scaffoldContext).showSnackBar(new SnackBar( content: new Text('Server error'), duration: new Duration(seconds: 5), action: new SnackBarAction(label: 'RETRY', onPressed: _retryPressed, ), )); } void _retryPressed() { _presenter.retryServerCall(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @ override void showErrorMessage ( ) { Scaffold . of ( _scaffoldContext ) . showSnackBar ( new SnackBar ( content : new Text ( 'Server error' ) , duration : new Duration ( seconds : 5 ) , action : new SnackBarAction ( label : 'RETRY' , onPressed : _retryPressed , ) , ) ) ; } void _retryPressed ( ) { _presenter . retryServerCall ( ) ; }

Voila!

What next?

This code tutorial includes an introduction to MVP. App architecture is probably the most important factor determining app longevity. With good architecture, it is easy to add or change features, and therefore, the app is a pleasure to maintain. So it’s well worth keeping an eye on Flutter Architecture Samples, a project inspired, in part, by Android Architecture Blueprints.

Check out Being a software developer in 2020!

Related