Introduction to Page Navigation and Route in Flutter

7 min read, 27 November, 2018

Non-trivial mobile apps usually have multiple screens or pages. The end user is presented one screen at a time. Different user actions trigger page navigations. If you have used other mobile app frameworks before, you may find the concepts of page navigation and routes are very familiar. Popular web development frameworks, e.g. React and Angular, also have similar functionality, see react-router and Angular router. Experiences with these frameworks can help you to understand page navigation in Flutter.

Basic concepts

Page navigation in different frameworks have a similar design. The framework usually maintains a stack of navigation history. Visiting new pages will push new elements to the stack, while returning back to preview pages will pop elements from the stack. The top element of the stack is the current page, while the bottom of the stack is the initial page, or the home page. This philosophy is the same as viewing web pages in browsers. When viewing web pages, we navigate to new pages by following hyperlinks, which pushes the new page into the stack. Clicking the back button navigates to the preview page, which pops current page from the stack.

In Flutter, we use Navigator for page navigation. Navigator has the methods push and pop to manage the navigation stack.

Simple page navigation

Let's start from a simple page navigation example. The code below shows a RaisedButton which triggers the navigation to a new page when pressed. In the onPressed handler, Navigator.of(context) gets an instance of Navigator from the BuildContext, then we call push to push a new MaterialPageRoute to the stack. MaterialPageRoute requires a WidgetBuilder to build the widget for the new page. Here we simply create a page with another RaisedButton displayed in the center. Navigator.pop(context) pops the current page from the navigation stack and returns to the previous page.

RaisedButton(
  child: Text('Show simple route'),
  onPressed: () => Navigator.of(context).push(MaterialPageRoute(
      builder: (context) => Scaffold(
            appBar: AppBar(
              title: Text('Simple route'),
            ),
            body: Center(
              child: RaisedButton(
                onPressed: () => Navigator.pop(context),
                child: Text('Return to main screen'),
              ),
            ),
          ))),
)

The push method of Navigator accepts objects of type Route. MaterialPageRoute is a subclass of Route which suits best for MaterialApp. MaterialPageRoute replaces the whole screen with the new widget. If we use Scaffold, we don't need to add a button to return back to preview page. Scaffold provides a built-in button on the toolbar to go back.

There are other subclasses of Route that can be used in different scenarios. We'll cover them in other articles.

Below is the screenshot of the page. The back arrow in the toolbar is provided by Scaffold.

Screenshot of simple route

Pass data between routes

When navigating between different pages, a typical requirement is to pass data between them. A master-details view needs to pass selected item from the master view to the details view. A page may open a form to collect user's input before it can continue.

  • Passing data to a page is done by setting directly in the WidgetBuilder.
  • Data can be passed back to preview page using Navigator.pop.

A route in Flutter is just a widget, so we can pass data to it in the constructor. Navigator.pop can accept an optional argument as the data to pass back. The return value of Navigator.push is actually a Future object which resolves to the value provided in Navigator.pop.

Let's see a complete example. ColorSelectorPage is a StatefulWidget that has a state color to set its background color. In the handler _showColorSelector of the button pressed event, we navigate to the ColorSelector page and use its return value to set the value of color. Here we use async and await to get value from the Future object.

When creating the ColorSelector, the current value of color is passed to it as the constructor argument. ColorSelector renders each available color as a button. Navigator.of(context).pop(color) returns the selected color to ColorSelectorPage.

import 'package:flutter/material.dart';

class ColorSelectorPage extends StatefulWidget {
  
  _ColorSelectState createState() {
    return _ColorSelectState();
  }
}

class _ColorSelectState extends State<ColorSelectorPage> {
  Color color;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Color selector example'),
      ),
      body: Container(
          color: color,
          child: Center(
            child: RaisedButton(
              child: Text('Select background color'),
              onPressed: _showColorSelector,
            ),
          )),
    );
  }

  _showColorSelector() async {
    color = await Navigator.of(context)
        .push(MaterialPageRoute(builder: (context) => ColorSelector(color)));
  }
}

class ColorSelector extends StatelessWidget {
  final List<Color> colors = [
    Colors.red,
    Colors.green,
    Colors.blue,
    Colors.orange,
  ];

  final Color selectedColor;

  ColorSelector(this.selectedColor);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Select a color'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Column(
              children: <Widget>[
                Text('Selected color: $selectedColor'),
                Column(
                  children: colors
                      .map<Widget>((color) => RaisedButton(
                            child: Text(color.toString()),
                            textColor: color,
                            onPressed: () => Navigator.of(context).pop(color),
                          ))
                      .toList(),
                )
              ],
            )
          ],
        ),
      ),
    );
  }
}

The screenshot below shows the ColorSelector page.

Screenshot of color selector

Named routes

All the routes created so far are using Navigator.push with WidgetBuilder directly. If we want to navigate to the same page in different places, it's hard to share the logic of page navigation. With named routes, we can assign unique identifiers to routes, and use these identifiers for page navigation. For React or Angular developers, route names are just like URLs.

It's a common practice to name routes using a URL-like pattern, e.g. /product/001 or /user/001/address.

Named routes are declared as the argument routes of type Map<String, WidgetBuilder> in MaterialApp. The map key is the route name, the value is the WidgetBuilder to create the page widget. Navigator.pushNamed is used to navigate to a named route.

NamedRouteWidget in the code below navigates to a random route with name /random/<number> after pressing the button. routeNumber is the assigned number for the current route, while numberOfRoutes is the total number of available routes.

import 'dart:math';

import 'package:flutter/material.dart';

class NamedRouteWidget extends StatelessWidget {
  final int routeNumber;
  final int numberOfRoutes;
  final Random random = Random();

  NamedRouteWidget(this.routeNumber, this.numberOfRoutes);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Random route $routeNumber'),
      ),
      body: Center(
        child: RaisedButton(
            child: Text('Go to a random route'),
            onPressed: () => Navigator.of(context)
                .pushNamed('/random/${random.nextInt(numberOfRoutes)}')),
      ),
    );
  }
}

In the code shown below, staticRoutes is the map that contains one named route / for the home page. _generateRoutes generates 50 named routes to the NamedRouteWidget with different names. routes contains both static routes and generated routes. initialRoute is the initial route, which set to /.

class MyApp extends StatelessWidget {
  Map<String, WidgetBuilder> staticRoutes = {
    '/': (context) => MyHomePage(title: 'Flutter Navigator Example'),
  };

  
  Widget build(BuildContext context) {
    Map<String, WidgetBuilder> routes = Map();
    routes.addAll(staticRoutes);
    routes.addAll(_generateRoutes());
    return MaterialApp(
      title: 'Flutter Navigator Example',
      theme: ThemeData(
        primarySwatch: Colors.indigo,
      ),
      initialRoute: '/',
      routes: routes,
    );
  }

  Map<String, WidgetBuilder> _generateRoutes() {
    int total = 50;
    return Map.fromIterable(
        List.generate(total, (value) => [value, _generateRoute(value, total)]),
        key: (pair) => '/random/${pair[0]}',
        value: (pair) => pair[1]);
  }

  WidgetBuilder _generateRoute(int number, int total) {
    return (context) => NamedRouteWidget(number, total);
  }
}

The screenshot shows the named routes.

Screenshot of named route

For the example of generated routes, it's better to use onGenerateRoute of MaterialApp. We'll talk about this in another article.

That's all for the basic introduction of page navigation and route in Flutter.

Source code

Source code of this article is available on GitHub.