Flutter Navigation – How to Prevent Navigation

This is the second article of a multi-part series where we will focus on how to navigate within a Flutter app.  In this article, we are going to go beyond the basics and cover how to prevent navigation within Flutter apps.  Why would you need to do this?  Well, you might want to prompt the user to perform some kind of task before leaving the page – such as filling out a form. Or you might want to add some additional logic that can check when a page or even the app itself can be navigated away from.  We’ll cover each scenario within this article.  If you are interested in reading this entire series, here are the links for you:

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…

The app consists of 3 pages: home, about, and my form.  You can follow along and build it as we go or you can download all of the source code here.  When the app is launched, you’ll be on the home page which contains a Drawer.  If you are not familiar with a Drawer, it is a slide-out menu that is invoked via a familiar hamburger icon.  Here are some screenshots of the app we will build:

 

 

 

 

 

 

 

 

The structure of this app will be comprised of 4 files –

Let’s begin by opening up your main.dart file and replacing it’s contents with the following code:

import 'package:flutter/material.dart';
import 'home.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: 'Flutter Nav Demo 2',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(),
    );
  }
}

What the above code does is setup a Flutter App and tell the Flutter framework to display by default an instance of MyHomePage.  Let’s go ahead and create the home page by adding a new file named home.dart and place within it the following code:

import 'package:flutter/material.dart';
import 'about.dart';
import 'myform.dart';

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

void _navAbout(BuildContext context) {
  Navigator.push(
      context, new MaterialPageRoute(builder: (context) => new AboutPage()));
}

void _navForm(BuildContext context) {
  Navigator.push(
      context, new MaterialPageRoute(builder: (context) => new MyFormPage()));
}

class _MyHomePageState extends State {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text("Navigation Demo"),
          backgroundColor: Colors.deepOrangeAccent,
        ),
        drawer: new Drawer(
          child: new ListView(
            children: [
              new ListTile(
                title: new Text("WELCOME"),
              ),
              new Divider(),
              new ListTile(
                  title: new Text("Sample Form"),
                  trailing: new Icon(Icons.edit),
                  onTap: () {
                    Navigator.of(context).pop();
                    _navForm(context);
                  }),
              new ListTile(
                  title: new Text("About"),
                  trailing: new Icon(Icons.info),
                  onTap: () {
                    Navigator.of(context).pop();
                    _navAbout(context);
                  }),
            ],
          ),
        ),
        body: new Center(
          child: new Text("Home Page", style: new TextStyle(fontSize: 35.0)),
        ));
  }
}

Within this code for the home page we have setup the Drawer widget with a ListView that contains two ListTile entries: One to represent the sample form page and one to represent the about page.  Tapping on a tile navigates the user to its corresponding page using logic that should be familiar to you if you followed along in the first post in this series.  This is a simple and effective way to create a navigation menu in Flutter.  Of course this code doesn’t work just yet – it doesn’t know about the sample form or the about page.  Let’s create both of them…

The About Page

Our about page will be a simple example of how to navigate via a button-click.  Nothing too exciting here and we have seen this before in our first post in this series.  Create a new file called about.dart and add in the following code:

import 'package:flutter/material.dart';

class AboutPage extends StatefulWidget {
  @override
  _AboutPageState createState() => new _AboutPageState();
}

void _navHome(BuildContext context) {
  Navigator.pop(context);
}

class _AboutPageState extends State<AboutPage> {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('About'),
        backgroundColor: Colors.deepOrangeAccent,
      ),
      body: new Center(
        child: new RaisedButton(
          child: new Text('Back to Home Page'),
          onPressed: () => _navHome(context),
        ),
      ),
    );
  }
}

The Sample Form Page

The sample form page will ultimately contain logic that shows you how to deal with prompting a user to complete a form before leaving a page.  But for now, let’s just get the bare bones in there so we can have a working app.  Create a file named myform.dart and place within it the following code:

import 'package:flutter/material.dart';

class MyFormPage extends StatefulWidget {
  @override
  _MyFormPageState createState() => new _MyFormPageState();
}

class _MyFormPageState extends State<MyFormPage> {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text('My Form'),
          backgroundColor: Colors.deepOrangeAccent,
        ),
        body: new Center(
          child: new Text("My Form Page", style: new TextStyle(fontSize: 35.0)),
        ));
  }
}

At this point your app should work, but we are not doing anything here to prevent navigation yet.  Let’s look at that next…

Prevent Exiting of a Flutter App

If you want to prevent a user from exiting your app, how would you do it in Flutter?  The easiest way is to utilize the WillPopScope class, which as the documentation reads: “Registers a callback to veto attempts by the user to dismiss the enclosing ModalRoute.“.  This is perfect for our needs.  As you might expect, this is simply a widget that needs to be placed within our tree of components on our home page.  A WillPopScope requires two properties to be set – a callback for onWillPop and a child property.  Let’s see the modified version of the build method within the home.dart code:

Widget build(BuildContext context) {
    return new WillPopScope(
        onWillPop: () => _exitApp(context),
        child: new Scaffold(
            appBar: new AppBar(
              title: new Text("Navigation 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("Sample Form"),
                      trailing: new Icon(Icons.edit),
                      onTap: () {
                        Navigator.of(context).pop();
                        _navForm(context);
                      }),
                  new ListTile(
                      title: new Text("About"),
                      trailing: new Icon(Icons.info),
                      onTap: () {
                        Navigator.of(context).pop();
                        _navAbout(context);
                      }),
                ],
              ),
            ),
            body: new Center(
              child:
                  new Text("Home Page", style: new TextStyle(fontSize: 35.0)),
            )));
  }

It is really only lines 2-4 that are changed.  We create a new WillPopScope widget and assign its child to the Scaffold and its onWillPop to a method we will create next called _exitApp.  The code for _exitApp is below, please add this to your home.dart file (directly above the build method will do just fine):

Future<bool> _exitApp(BuildContext context) {
  return showDialog(
        context: context,
        child: new AlertDialog(
          title: new Text('Do you want to exit this application?'),
          content: new Text('We hate to see you leave...'),
          actions: <Widget>[
            new FlatButton(
              onPressed: () => Navigator.of(context).pop(false),
              child: new Text('No'),
            ),
            new FlatButton(
              onPressed: () => Navigator.of(context).pop(true),
              child: new Text('Yes'),
            ),
          ],
        ),
      ) ??
      false;
}

What the above method does is put together a callback that onWillPop will call whenever the Navigator is asked to navigate away from this page via a pop().  If you look at the typedef for onWillPop, you will see that it expects the signature of your callback to return a Future<bool> and our callback does just that by utilizing a simple AlertDialog to ask the user if they really want to exit the app.  This same approach can be used on any of your pages within your app, not just the home page.

Prompt a User to Complete a Form Before Leaving a Page

One of the more common use cases of any application that accepts input from a user, is to make sure that the form is properly filled out before leaving a page.  Fortunately for us, there is a facility baked into Flutter forms that allow us to invoke a callback that can be used to perform a check before we allow a user to navigate away from a page that contains a form.  It is fairly simple to implement – let’s take a look!

First, here is how you specify a callback on a form:

onWillPop: () => _canLeave(context),

And here is the implementation of that callback:

Future<bool> _canLeave(BuildContext context) {
    if (saved) {
      return new Future<bool>.value(true);
    } else
      return _prompt(context);
}

What the above code does is it checks to see if the form has been saved yet.  If the form is valid, the navigation is allowed.  Here is the complete code you can use for myform.dart to see this all in action:

import 'dart:async';

import 'package:flutter/material.dart';

class MyFormPage extends StatefulWidget {
  @override
  _MyFormPageState createState() => new _MyFormPageState();
}

class _MyFormPageState extends State<MyFormPage> {
  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
  final formKey = new GlobalKey<FormState>();
  bool saved = false;

  void _submit() {
    final form = formKey.currentState;
    if (!form.validate()) {
      return;
    }

    saved = true;
    _scaffoldKey.currentState.showSnackBar(new SnackBar(
        backgroundColor: Colors.black, content: new Text("Saved!")));
  }

  Future<bool> _canLeave(BuildContext context) {
    if (saved) {
      return new Future<bool>.value(true);
    } else
      return _prompt(context);
  }

  Future<bool> _prompt(BuildContext context) {
    return showDialog(
          context: context,
          child: new AlertDialog(
            title: new Text('Warning - Incomplete form'),
            content: new Text('Do you want to stay and complete this form?'),
            actions: <Widget>[
              new FlatButton(
                onPressed: () => Navigator.of(context).pop(true),
                child: new Text('No'),
              ),
              new FlatButton(
                onPressed: () => Navigator.of(context).pop(false),
                child: new Text('Yes'),
              ),
            ],
          ),
        ) ??
        false;
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      key: _scaffoldKey,
      appBar: new AppBar(
        title: new Text('My Form'),
        backgroundColor: Colors.deepOrangeAccent,
      ),
      body: new Padding(
        padding: const EdgeInsets.all(16.0),
        child: new Form(
          key: formKey,
          autovalidate: true,
          child: new Column(
            children: [
              new TextFormField(
                  decoration: new InputDecoration(labelText: 'Enter something'),
                  validator: (val) =>
                      val.isEmpty ? "You must enter something" : null),
              new RaisedButton(
                onPressed: _submit,
                child: new Text('Save'),
              ),
            ],
          ),
          onWillPop: () => _canLeave(context),
        ),
      ),
    );
  }
}

Summary

Although as of this writing Flutter is somewhat of a new platform, you can tell the engineers at Google have put a lot of thought into the various navigation scenarios that developers are going to encounter.  Having the ability to invoke callbacks before a navigation happens is extremely useful and the examples I have shown here are a good starting point to incorporate into your app.  Happy coding!

2 thoughts on “Flutter Navigation – How to Prevent Navigation”

Leave a Reply

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