Flutter – How to Build a Modal Progress Indicator

Sometimes when you need to perform a lengthy process it is best to keep the user on the current page and show them an indication that the system is busy processing their request.  A great way to achieve this is via a progress indicator – one that we can make modal.  In this article I’ll show you how to build a simple modal progress indicator in Flutter.

Creating the App

If you are not yet familiar with the basics of creating a flutter app, please see my first post in this series on flutter – From Zero to App with Flutter.

Note – for this post we’ll be using VS Code although other IDEs may be used.  Open up VS Code and from the command palette (CTRL+SHIFT+P) choose ‘Flutter:New Project’.  Enter a new project name and hit Enter.  VS Code will setup your new project, which we will be heavily modifying next…

A Simple Menu Using Drawer

Now that you have the app created, go ahead and open the main.dart file.  What we will do is replace the existing code with being a simple declaration of a Drawer. This will create a nice looking slide-in left navigation menu that we often refer to as a hamburger menu.  Go ahead and replace the entire contents of main.dart with the following:

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Progress Indicator Demo',
      theme: new ThemeData(
        primarySwatch: Colors.orange,
      ),
      home: new MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text("Progress Indicator Demo"),
          backgroundColor: Colors.deepOrangeAccent,
        ),
        drawer: new Drawer(
          child: new ListView(
            children: <Widget>[
              new ListTile(
                title: new Text("WELCOME"),
              ),
              new Divider(),
              new ListTile(
                  title: new Text("Settings"),
                  trailing: new Icon(Icons.settings),
                  onTap: () {}),
            ],
          ),
        ),
        body: new Center(
          child: new Text("Home Page", style: new TextStyle(fontSize: 35.0)),
        ));
  }
}

Now, if you run your flutter application, here is what you would see:

 

Creating a Settings Page

You probably noticed in our menu we had an item labeled ‘Settings’.  What we want to do is create a page where the user can fill in some app-specific settings.  In a real app, this might post their settings to a back-end service and perhaps, especially on a slow connection, this might take a few seconds or more.  Let’s create that settings page.  First, create a new file and name it settings.dart.  Place within it the following code:

import 'package:flutter/material.dart';

class SettingsPage extends StatefulWidget {
  @override
  _SettingsPageState createState() => new _SettingsPageState();
}

class _SettingsPageState extends State<SettingsPage> {
  bool _monitor = true;
  bool _lights = false;
  bool _kitchen = false;
  bool _bedroom = false;

  void _submit() {
  }

  List<Widget> _buildForm(BuildContext context) {
    Form form = new Form(
      child: new Column(
        children: [
          new CheckboxListTile(
            title: const Text('Enable Monitoring?'),
            value: _monitor,
            onChanged: (bool value) {
              setState(() {
                _monitor = value;
              });
            },
            secondary: const Icon(Icons.power),
          ),
          new SwitchListTile(
            title: const Text('Lights'),
            value: _lights,
            onChanged: (bool value) {
              setState(() {
                _lights = value;
              });
            },
            secondary: const Icon(Icons.lightbulb_outline),
          ),
          new SwitchListTile(
            title: const Text('Kitchen'),
            value: _kitchen,
            onChanged: (bool value) {
              setState(() {
                _kitchen = value;
              });
            },
            secondary: const Icon(Icons.kitchen),
          ),
          new SwitchListTile(
            title: const Text('Bedroom'),
            value: _bedroom,
            onChanged: (bool value) {
              setState(() {
                _bedroom = value;
              });
            },
            secondary: const Icon(Icons.hotel),
          ),
          new RaisedButton(
            onPressed: _submit,
            child: new Text('Save'),
          ),
        ],
      ),
    );

    var l = new List<Widget>();
    l.add(form);
    return l;
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Settings'),
        backgroundColor: Colors.deepOrangeAccent,
      ),
      body: new Stack(
        children: _buildForm(context),
      ),
    );
  }
}

Navigating to our Settings Page

At this point we have a simple settings page, but no way to get there.  Let’s go back to our main.dart file and fix that.  You’ll need to add an import statement for your settings page as follows:

import 'settings.dart';

Next, locate the onTap event that we have for the settings page.  Currently it has an empty body.  Let’s update the onTap to navigate to our settings page as follows:

Navigator.of(context).pop();
  Navigator.push(
      context,
      new MaterialPageRoute(
          builder: (context) => new SettingsPage()));

Note that this is just one way to navigate.  For more information on navigation, please refer to my series of articles on navigation.

Building a Modal Progress Indicator

What we are going to achieve in this section is to modify our settings page so that when the user submits their changes, a nice looking modal progress indicator appears.  We will do this by combining the ModalBarrier and CircularProgressIndicator.  The ModalBarrier will create a modal overlay that will prevent the user from interacting with the page behind it and the CircularProgressIndicator will show the user the system is processing their request.  In order to achieve this we are going to make some modifications to settings.dart.

First, add the following import to settings.dart:

import 'dart:async';

And declare the following class variable within _SettingsPageState:

bool _saving = false;

Next, replace the existing _submit method with the following code:

void _submit() {
    print('submit called...');

    setState(() {
      _saving = true;
    });

    //Simulate a service call
    print('submitting to backend...');
    new Future.delayed(new Duration(seconds: 4), () {
      setState(() {
        _saving = false;
      });
    });
  }

So… What is it that we have just done?  Well, we have created a method that when called will perform a long running operation (in our case 4 seconds) and during that time frame the user won’t have any indication that the application is processing a request on their behalf.  Of course in our case we simulated the long running operation by using Future.delayed but this is where you would end up calling some sort of service that would handle the request.

But what is the deal with those setState() calls?  According to the Flutter docs, setState notifies the framework that the internal state of this object has changed in a way that might impact the user interface in this subtree, which causes the framework to schedule a build for this State object.  Why does that matter to us?  Because we want to modify the user interface to display our progress indicator only when _saving is in a state of true.  Here is how we will do just that – within the _buildForm method, add the following code just above the return statement:

    if (_saving) {
      var modal = new Stack(
        children: [
          new Opacity(
            opacity: 0.3,
            child: const ModalBarrier(dismissible: false, color: Colors.grey),
          ),
          new Center(
            child: new CircularProgressIndicator(),
          ),
        ],
      );
      l.add(modal);
    }

So here is how this all works –

  • The _submit() method is invoked by a button click
  • Within a call to setState() _saving is set to true
  • Flutter schedules a call to the build() method which ends up rebuilding our form
  • Inside of our build we see that _saving is true and a modal progress indicator is added to the user interface
  • Once our long running process has completed, _saving is set to false within another call to setState()
  • Flutter schedules another call to the build() method
  • In this go around _saving is found to be false and our modal progress indicator is not included in the next rendering of our user interface.

Let’s Run that App!

To see this in action, launch your app and click on the Settings menu.  While on the settings page, click the Save button.  You should see a circular progress indicator display in the middle of the page and the user interface elements cannot be clicked due to the modal barrier that is constructed.  After the simulated call to a back end service completes, the progress indicator disappears and the form is once again accessible.

Summary

Flutter continues to impress me with the number of widgets that are available – especially considering Flutter is still in beta!  By combining two widgets together (in this case ModalBarrier and CircularProgressIndicator), we can put together a very useful UI composed of just what we need to get the job done.  I hope you have enjoyed this article and learned something along the way.

If you would like to, you can grab the source code for this demo app.

2 thoughts on “Flutter – How to Build a Modal Progress Indicator”

Leave a Reply

Your email address will not be published. Required fields are marked *