Modal named routes in Flutter

Flutter’s navigation and routing definitions are great tools for handling moving from one page of an application to another and back. In particular named routes make it simple to decouple implementation from intent. The standard navigation behaviour using named routes is to create a MaterialPageRoute using a widget. If you want to use a popup route or some other modal artifact that doesn’t work. Turns out it’s simple to override this behaviour and this post explains how.

Basic named routes

Using navigation with named routes is pretty simple. First declare the route in the routes table as part of the MaterialApp definition. Then use it with a navigator, those two pieces look like this:

MaterialApp(
  title: "Routing Demonstration",
  routes: {
    '/': (context) => RoutingHomePage(),
    MySecondScreen.routeName: (context) => MySecondScreen(),
  }
),

....
// In some other method use the named route
  Navigator.pushNamed( context, MySecondScreen.routeName );

The limitation to this mechanism is that it automatically creates a MaterialPageRoute to wrap the widget that is provided as the entry to the routes table. In the example above the two widgets are RoutingHomePage and MySecondScreen. When the pushNamed method is called on the navigator a new MaterialPageRoute that refers to one of those widgets is created. That works fine to completely replace the current page, but not for popups.

Handling custom routes

There is a mechanism that allows particular named routes to be handled differently while still using the routes table for all of the common cases. The trick is that there is an onGenerateRoute property on the MaterialApp class that takes a function that dynamically generates routes for a request. That function is only invoked if the requested route isn’t defined in the routes table. The documentation for the onGenerateRoute property explains this.

Given that behaviour here’s how to handle a call to pushNamed by creating a popup or other route.

MaterialApp(
  title: "Routing Demonstration",
  onGenerateRoute: _generateRoute
  routes: {
    '/': (context) => RoutingHomePage(),
    MySecondScreen.routeName: (context) => MySecondScreen(),
  }
),
...

Route _generateRoute( RouteSettings settings ) {
  switch (settings.name) {
    case MyPopupScreen.routeName:
      return MyPopupRoute();
      break;
  }
  return null;
}

...
// In some other method use the named route
  Navigator.pushNamed( context, MyPopupRoute.routeName );

Note that the routes table hasn’t changed. What’s new is the onGenerateRoute property and the _generateRoute function. Now _generateRoute will be called whenever a named route is requested that isn’t present in the routes table. Since MyPopupRoute.routeName isn’t in that table, when that route is requested in a pushNamed call, the _generateRoute function is invoked. Note that _generateRoute returns a Route, not a Widget. The MyPopupRoute class referenced above extends PopupRoute to satisfy that signature. It would look something like this:

class MyPopupRoute extends PopupRoute {
  static const String routeName = "/mypopup";

  Widget buildPage ( BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation ) {
        return Placeholder();
    }
 }

Leave a Reply