One of the more verbose parts of Flutter is pushing new pages into the Navigator Stack, especially if you want to use something other than the standard Material or Cupertino Routes.

The standard recommendation from Flutter Docs, is to just build your PageRoutes inside your button handlers, and then pass them directly into Navigator from your views. Something like:

Navigator.of(context).push(MaterialPageRoute(builder: (c)=> MyPage()))

This works, but can end duplicated the same code all around your code base replicating the same transitions, violating the core tenant of DRY programming. Additionally if you’re using custom routes, then you’ll be forced to create additional widgets for each type, or worse, copy tons of boilerplate from view to view.

In a recent talk at a Flutter Europe, there was an interesting approach floated by Flutter Community Manager Simon Lightfoot: Define a static .route() function on each view, and have that function return the page route. So:

//Instead of: Navigator.of(context).push(MaterialPageRoute(builder: (c)=> MyPage())) //Do something like: Navigator.of(context).push(MyPage.route());

And let the Widget itself define the route:

lass MyPage extends StatelessWidget { static Route<dynamic> route() => MaterialPageRoute(builder: (c)=> MyPage()); }

This is an nice approach, with the rationale being that it helps keep your code DRY and more maintainable, which is true! However, we still see a couple small issues:

It binds the view to a specific transition type. In the world of Flutter, were a widget can be composed and displayed in various contexts, on different form factors, it feels like the transition should not be something that the view itself cares about. It’s more flexible if the view does not know or care how it is placed into the view port.

In the world of Flutter, were a widget can be composed and displayed in various contexts, on different form factors, it feels like the transition should not be something that the view itself cares about. It’s more flexible if the view does not know or care how it is placed into the view port. It’s still not that DRY. If you have many views, you will still end up duplicating the route building code all around your codebase, or having to create discrete route widgets to encapsulate that code.

These are by no means deal breakers, and the above approach is still a solid option, but having faced some similar challenges, we’ve landed on a slightly different method.

PageRoutes.dart

Instead of scattering the Route definitions around your code base, or including them in each view, we like to use a shared static class, with defined builders that you can call from anywhere in your code base.

Here’s a simple example, of 2 routes, Fade, and Slide. They have customizable duration and easing, and Slide allows you to easily transition in from any position you like. Most of this is boilerplate, the actual code to do the Transition is pretty succinct:

typedef Widget PageBuilder(); class PageRoutes { static const double kDefaultDuration = .35; static const Curve kDefaultEaseFwd = Curves.easeOut; static const Curve kDefaultEaseReverse = Curves.easeOut; static Route<T> fade<T>(PageBuilder page, [double duration = kDefaultDuration]) { return PageRouteBuilder<T>( transitionDuration: Duration(milliseconds: (duration * 1000).round()), pageBuilder: (context, animation, secondaryAnimation) => page(), transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition(opacity: animation, child: child); }, ); } static Route<T> slide<T>(PageBuilder page, {double duration = kDefaultDuration, Offset startOffset = const Offset(1, 0), Curve easeFwd = kDefaultEaseFwd, Curve easeReverse = kDefaultEaseReverse}) { return PageRouteBuilder<T>( transitionDuration: Duration(milliseconds: (duration * 1000).round()), pageBuilder: (context, animation, secondaryAnimation) => page(), transitionsBuilder: (context, animation, secondaryAnimation, child) { bool reverse = animation.status == AnimationStatus.reverse; return SlideTransition( position: Tween<Offset>(begin: startOffset, end: Offset(0, 0)) .animate(CurvedAnimation(parent: animation, curve: reverse ? easeReverse : easeFwd)), child: child, ); }, ); }

Now, you can push a page route from anywhere in your code just like so:

Navigator.of(context).push(PageRoutes.slide(()=>MyPage()));

These above routes were kept fairly simple, but could easily be extended to add cross fading, and cupertino style “push” routes or really any custom translation you can think of. It’s also very easy to add little utility methods, like slideUp or slideLeft, adding syntactic sugar to your code, and reducing boilerplate.

Another thing this lets you do, is define a defaultRoute(), that you can easily change later. For example, if you want to use Material Routes throughout your application, you can simply do:

class PageRoutes { static Route<T> defaultRoute<T>(PageBuilder page) => MaterialPageRoute(builder: (c)=> page() } ... //And then push like this: Navigator.of(context).push(PageRoutes.defaultRoute(()=>MyPage()));

Then, if you need to change it later, it’s just a single line change.

What we really like about this approach is it allows you to create totally custom routes, with as many params as you need, without having a bunch of code scattered or repeated around your codebase. It also keeps everything in a single file, keeping all your routes nice and portable, and allowing you to re-use and evolve them from project to project.

Internally we have many more page routes defined than this, but this should give you a good head start! If you’d like to take this to the next level, check out our post on the new animations package, the FadeThrough, SharedAxis and FadeScale page-routes will drop right into the static class above and you’re good to go!

Until next time!