21 Route and Navigation Flutter Implements Page Switching Like This

21 Route and Navigation Flutter Implements Page Switching Like This #

Hello, I am Chen Hang.

In the previous article, I taught you how to implement cross-component data transfer in Flutter. Among them, InheritedWidget is suitable for scenarios where the child widget needs to share data from the parent widget across layers. If the child widget also needs to modify the data of the parent widget, it needs to be used in conjunction with State. Notifications are suitable for scenarios where the parent widget listens for events from the child widget. For communication between non-parent-child related parties, we can also use EventBus to implement data interaction based on the subscribe/publish pattern.

If we say that the basic unit of the UI framework’s view elements is components, then the basic unit of an application is pages. For applications with multiple pages, we need a unified mechanism to smoothly transition from one page to another. This mechanism is usually called route management or navigation management.

First, we need to know the target page object and open it using the framework’s provided method after the target page is initialized. For example, in Android/iOS, we usually initialize an Intent or a ViewController and open a new page using startActivity or pushViewController. In React, we use navigation to manage all pages, and as long as we know the name of the page, we can navigate to it immediately.

In fact, Flutter’s route management also draws on these two design ideas. So today, let’s take a look at how to manage the naming and transition of different pages in a Flutter application.

Route Management #

In Flutter, page navigation is managed through Routes and Navigator:

  • Route is an abstraction of a page, responsible for creating the corresponding interface, receiving parameters, and responding to the opening and closing of Navigator.
  • Navigator maintains a route stack to manage Routes. A Route is pushed onto the stack when it is opened, and popped from the stack when it is closed. Additionally, an existing Route in the stack can be directly replaced.

Based on whether the page identifier needs to be registered in advance, there are two types of route management in Flutter:

  • Basic routes: No need to register in advance. You need to construct the page instance when switching pages.
  • Named routes: The page identifier needs to be registered in advance. You can directly open a new route by using the identifier when switching pages.

Next, let’s take a look at the basic route management.

Basic Routes #

In Flutter, the usage of basic routes is very similar to how new pages are opened in Android/iOS. To navigate to a new page, we need to create an instance of MaterialPageRoute and call the Navigator.push method to push the new page onto the top of the stack.

MaterialPageRoute is a route blueprint that defines the configuration for route creation and transition animations. It allows the implementation of route transition animations consistent with the style of platform page transitions on different platforms.

If we want to go back to the previous page, we need to call the Navigator.pop method to remove the page from the stack.

The following code demonstrates the usage of basic routes: opening the second page in the button event of the first page, and going back to the first page in the button event of the second page:

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      // Open the page
      onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SecondScreen())),
    );
  }
}

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      // Go back to the previous page
      onPressed: () => Navigator.pop(context),
    );
  }
}

Running the code, the effect is as follows:

Figure 1 Basic route example

As we can see, the usage of basic routes is quite simple. Next, let’s move on to the usage of named routes.

Named Routes #

The usage of basic routes is relatively simple and flexible, suitable for scenarios with few pages in the application. However, in cases where the application has many pages, using basic routes every time we switch to a new page requires manually creating MaterialPageRoute instances, initializing pages, and then opening them, which can be tedious.

To simplify route management, Flutter provides another way-called named routes. We give a name to each page, and then we can directly open it by using its name. This method is simple and intuitive, similar to the navigation usage in React.

To switch pages using names, we need to provide a page name-to-widget mapping rule to the MaterialApp widget of the application, which serves as the route table. This way, Flutter knows the corresponding relationship between the name and the corresponding page widget.

The route table is actually a Map, where the key corresponds to the page name, and the value is a WidgetBuilder callback function where we create the corresponding page. Once the page name is defined in the route table, we can use Navigator.pushNamed to open the page.

The following code demonstrates the usage of named routes: the ‘second_page’ name is registered and bound to the initialization method of the SecondPage in the MaterialApp widget. Later, we can open the page using the name ‘second_page’ in the code:

MaterialApp(
  ...
  // Register routes
  routes: {
    "second_page": (context) => SecondPage(),
  },
);
// Open the page using the name
Navigator.pushNamed(context, "second_page");

As we can see, the usage of named routes is also simple.

However, since both the registration and usage of routes use strings as identifiers, there is a potential risk: what will happen if we open a route that does not exist?

You may think that we can use string constants to define and use routes, but we can’t avoid the scenario of using incorrect route identifiers from the interface data. Faced with this situation, neither directly reporting an error nor ignoring incorrect routes are good solutions for providing a good user experience.

A better approach is to provide a friendly error message to the user, such as jumping to a unified NotFoundScreen page, which also makes it easier for us to collect and report these types of errors centrally.

When registering the route table, Flutter provides the UnknownRoute property, which allows us to handle unknown route identifiers with a unified page navigation.

The following code shows how to register an error route handler. Similar to the usage of basic routes, we only need to return a fixed page.

MaterialApp(
  ...
  // Register the route table and handle unknown routes
  onUnknownRoute: (RouteSettings settings) {
    return MaterialPageRoute(builder: (context) => NotFoundScreen());
  },
);

As we can see, the usage of error routes is also quite simple.

MaterialApp(
    ...
    // Register routes
    routes: {
        "second_page": (context) => SecondPage(),
    },
    // Error route handling, uniformly return UnknownPage
    onUnknownRoute: (RouteSettings setting) => MaterialPageRoute(builder: (context) => UnknownPage()),
);

// Open page with incorrect name
Navigator.pushNamed(context, "unknown_page");

By running the code above, we can see that our application can not only handle correct page route identifiers, but also uniformly jump to a fixed error handling page for incorrect page route identifiers.

Screenshot

Figure 2. Named route example

Page Parameters #

Unlike basic route, which can precisely control the initialization of the target page, named route can only initialize the fixed target page through a string name. In order to meet the initialization requirements of the target page in different scenarios, Flutter provides a mechanism for route parameters, which can pass related parameters when opening a route, and obtain the page parameters through RouteSettings in the target page.

The following code demonstrates how to pass and obtain parameters: when opening the page with the name “second_page”, a string parameter is passed, which is then retrieved in SecondPage and displayed in the text.

// Pass string parameter when opening the page
Navigator.of(context).pushNamed("second_page", arguments: "Hey");

class SecondPage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        // Retrieve route parameters
        String msg = ModalRoute.of(context).settings.arguments as String;
        return Text(msg);
    }
}

In addition to passing parameters when opening a page, for specific pages, parameters also need to be passed when closing the page to inform the page of the processing results.

For example, in an e-commerce scenario, when a user adds a product to the shopping cart, the login page is opened for the user to log in. After the login operation is completed, when the login page is closed and returned to the current page, the login page informs the current page of the new user identity, and the current page refreshes itself with the new user identity.

Similar to the startActivityForResult method provided by Android to listen to the processing results of the target page, Flutter also provides the mechanism of return parameters. When pushing the target page, you can set a listener function for the target page to obtain the return parameters; and the target page can pass related parameters when closing the route.

The following code demonstrates how to obtain parameters: when SecondPage is closed, a string parameter is passed, which is then retrieved in the listener function of the previous page and displayed.

class SecondPage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return Scaffold(
            body: Column(
                children: <Widget>[
                    Text('Message from first screen: $msg'),
                    RaisedButton(
                        child: Text('Back'),
                        // Pass parameters when closing the page
                        onPressed: () => Navigator.pop(context, "Hi")
                    )
                ]
            )
        );
    }
}

class _FirstPageState extends State<FirstPage> {
    String _msg = '';
    
    @override
    Widget build(BuildContext context) {
        return new Scaffold(
            body: Column(
                children: <Widget>[
                    RaisedButton(
                        child: Text('Named Route (Parameter & Callback)'),
                        // Open the page and listen for the parameters passed when closing the page
                        onPressed: () => Navigator.pushNamed(context, "third_page", arguments: "Hey").then((msg) => setState(() => _msg = msg)),
                    ),
                    Text('Message from Second screen: $_msg'),
                ],
            ),
        );
    }
}

After running, it can be seen that when closing SecondPage and returning to FirstPage, FirstPage displays the received msg parameter:

Screenshot

Figure 3. Page route parameters

Summary #

Alright, that’s all for today’s sharing. Let’s briefly review the main points.

Flutter provides two ways to manage page navigation: basic routing and named routing. With basic routing, you need to manually create page instances and use Navigator.push to navigate between pages. With named routing, you need to register the route names and page creation methods in advance, and then use Navigator.pushNamed with the corresponding route name to navigate between pages.

For named routing, if we need to handle invalid route names, we also need to register an UnknownRoute. In order to have fine-grained control over the route transition, Flutter provides parameters for page opening and closing. We can retrieve the parameters when creating a page and when the target page is closed.

As we can see, when it comes to route navigation, Flutter combines the features of Android, iOS, and React, making it simple yet powerful.

In medium to large-scale applications, we usually use named routing to manage page transitions. The most important feature of named routing is that it establishes a mapping between string identifiers and various pages, allowing complete decoupling between pages. Page transitions within the application can be easily achieved using just a string identifier, laying a solid foundation for future modularization.

I have packaged the knowledge covered in today’s sharing on GitHub. You can download the project to your local machine and run it multiple times to deepen your understanding of basic routing, named routing, and the usage of route parameters.

Thought Questions #

Finally, I’ll leave you with two small assignments.

  1. How do we pass page parameters for basic routing?
  2. Please implement a calculator page that can calculate the sum of two numerical parameters passed from the previous page, and inform the previous page of the result when this page is closed.

Feel free to leave a comment in the comment section to share your thoughts. I’ll be waiting for you in the next article! Thank you for listening, and please feel free to share this article with more friends to read together.