Flutter Routes & Navigation – Parameters, Named Routes, onGenerateRoute

Routing is one of the most basic things your app must have to do anything meaningful. However, navigating between pages can quickly turn into a mess. It doesn't have to be so!

There are multiple options for routing. Some create a lot of clutter, others cannot facilitate passing data between routes, and yet others require that you set up a third-party library. The option that you are going to learn about in this tutorial is the best of both worlds - first-party and yet clean to use.

Initial setup

Before we can do routing the right way, we first need to have some pages to navigate between. While setting them up, it's also not bad to showcase the most basic way of navigation from which we want to get away.

There are 2 pages and the second page receives data from the first one. We push MaterialPageRoutes directly to the navigator which creates quite a lot of boilerplate code. The more pages your app has, the worse it gets, and it's easy to get lost in all these routes specified all over the place.

main.dart

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      // Initially display FirstPage
      home: FirstPage(),
    );
  }
}

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Routing App'),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Text(
              'First Page',
              style: TextStyle(fontSize: 50),
            ),
            RaisedButton(
              child: Text('Go to second'),
              onPressed: () {
                // Pushing a route directly, WITHOUT using a named route
                Navigator.of(context).push(
                  // With MaterialPageRoute, you can pass data between pages,
                  // but if you have a more complex app, you will quickly get lost.
                  MaterialPageRoute(
                    builder: (context) =>
                        SecondPage(data: 'Hello there from the first page!'),
                  ),
                );
              },
            )
          ],
        ),
      ),
    );
  }
}

class SecondPage extends StatelessWidget {
  // This is a String for the sake of an example.
  // You can use any type you want.
  final String data;

  SecondPage({
    Key key,
    @required this.data,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Routing App'),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Text(
              'Second Page',
              style: TextStyle(fontSize: 50),
            ),
            Text(
              data,
              style: TextStyle(fontSize: 20),
            ),
          ],
        ),
      ),
    );
  }
}
While navigation without using named routes is OK for smaller projects, in more complex apps it adds code duplication. This is especially true if you have a route guard to only allow signed-in users enter certain pages, or any other kind of logic which needs to run as the user navigates.

The better way to navigate

Now that you know what non-named navigation looks like, wouldn't it be simpler to just use the code below which uses named routes?

...
// Pushing a named route
Navigator.of(context).pushNamed(
  '/second',
  arguments: 'Hello there from the first page!',
);
...

You have two options for navigating with named routes without needing a library. The first one is the simplest - just specify a map of routes on MaterialApp widget, its keys being the names of those routes. As soon as you want to pass some data between pages, and let alone run logic, this first option comes out of the equation. You cannot pass dynamic additional data in a map literal, after all!

The second option is to specify a function returning a route. By doing this, you still get the benefits of using named routes, but you now have the option to pass data to pages. This is possible, because unlike with a map literal, you can add logic to a function.

Creating a route_generator

The function which you need to specify on the root widget MaterialApp is called onGenerateRoute. It's good to separate your code into multiple classes and stand-alone functions though, so let's create a RouteGenerator class to encapsulate the routing logic.

route_generator.dart

import 'package:flutter/material.dart';
import 'package:routing_prep/main.dart';

class RouteGenerator {
  static Route<dynamic> generateRoute(RouteSettings settings) {
    // Getting arguments passed in while calling Navigator.pushNamed
    final args = settings.arguments;

    switch (settings.name) {
      case '/':
        return MaterialPageRoute(builder: (_) => FirstPage());
      case '/second':
        // Validation of correct data type
        if (args is String) {
          return MaterialPageRoute(
            builder: (_) => SecondPage(
                  data: args,
                ),
          );
        }
        // If args is not of the correct type, return an error page.
        // You can also throw an exception while in development.
        return _errorRoute();
      default:
        // If there is no such named route in the switch statement, e.g. /third
        return _errorRoute();
    }
  }

  static Route<dynamic> _errorRoute() {
    return MaterialPageRoute(builder: (_) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Error'),
        ),
        body: Center(
          child: Text('ERROR'),
        ),
      );
    });
  }
}

As you can see, you've moved from having bits of routing logic everywhere around your codebase, to a single place for this logic - in the RouteGenerator. Now, the only navigation code which will remain in your widgets will be the one pushing named routes with a navigator.

Before you can run and test the app, there's still a bit of a setup to do for this RouteGenerator to function.

main.dart

...

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      ...
      // Initially display FirstPage
      initialRoute: '/',
      onGenerateRoute: RouteGenerator.generateRoute,
    );
  }
}

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
            RaisedButton(
              child: Text('Go to second'),
              onPressed: () {
                // Pushing a named route
                Navigator.of(context).pushNamed(
                  '/second',
                  arguments: 'Hello there from the first page!',
                );
              },
            )
      ...
  }
}

...

With this done, run the app and test it! You can also intentionally mess up the name of the route to which you navigate or pass in an argument of a wrong type and see what happens.

Conclusion

You have learned how to navigate around your Flutter apps in a way suitable for larger apps. You've created a RouteGenerator which can encapsulate all of the routing logic - sparing you from code duplication. Creating many smaller classes with a certain purpose is always a good way to simplify your code and when it comes to routing, this principle still holds true.

Be sure to check out the video tutorial for a more hands-on perspective of building this app.

Icons made by Freepik from www.flaticon.com is licensed by CC 3.0 BY
Matej Rešetár
 

Matej is an app developer with a knack for teaching others. If he's not programming, making tutorials or doing other business, he's mostly working out, listening to audiobooks and taking cold showers.

>