Flutter: An Easy and Pragmatic Approach to Navigator 2.0
By: Marco Muccinelli
Tags: Navigation, Mobile App Development, Flutter, App Development, Flutter App Development

Flutter: An Easy and Pragmatic Approach to Navigator 2.0

Despite it looks intimidating at first glance, the evolved Flutter navigation system could solve more issues than it seems to create Marco Muccinelli Follow Feb 28 · 5 min read

During the second half of 2020, the Flutter team introduced a revamped navigation and routing system called Navigator 2.0. The reception by the developer community has not been great so far: principal criticisms regard the API complexity and the absence of an extra abstraction layer for common scenarios.

The great article written by John Ryan deeply analyzes the complexity and the completeness of Navigator 2.0

Besides, the naming has been terrible: Navigator 2.0 sounds like a technology the trumps the predecessor — Navigator 1.0 — while that’s simply not true. You can still use Navigator.push as long as you want. This doesn’t mean that you wouldn’t lose many advantages given by the new API, such as:

a more Flutterish-reactive declarative style;

better support for screen replacing;

better support for multiple pushes and pops;

full support to incoming URLs (e.g.: Flutter Web and/or app deep linking);

full support to browser history while using Flutter Web.

A pragmatic approach

I don’t want to spend too much time on technicalities but I’d like to dive directly into a convenient solution. For study purposes, I want to create an app with bottom tabs and a potentially infinite series of pages.

Modeling the pages

First of all, let’s choose a good abstraction to model the pages the user could navigate. Using directly a Flutter Page would defy the abstraction process, so I’ll use a data model using the superb union classes provided by frezeed package:

For further insights about freezed, please read my other article, Flutter: Dart Immutable Objects and Values

NavigationStackItem.notFound represents the 404 page;

represents the 404 page; NavigationStackItem.appSection represents a selected bottom bar tab;

represents a selected bottom bar tab; NavigationStackItem.ingredient represents the ingredient detail page;

represents the ingredient detail page; NavigationStackItem.recipe represents the recipe detail page.

Modeling user actions

The best data structure to reflect actions a user could make during navigation is the stack:

Please note that we always want to keep copies of items list, since Dart lists are not immutable

push corresponds to a new screen pushed onto the navigation stack;

corresponds to a new screen pushed onto the navigation stack; pop corresponds to a screen popped out from the navigation stack;

corresponds to a screen popped out from the navigation stack; it’s always possible to replace the entire navigation stack by setting items property. This could be useful for multiple reasons, like popping or pushing multiple pages at once, change the bottom tab selection from a deep page or even push a page before the current screen.

Make the navigation stack accessible

The main navigation stack will be part of the top app state. Instead of using a singleton — or worst, a global variable — I chose to adopt a ChangeNotifierProvider from the riverpod package:

The first stack content is the first selected bottom tab

Once you configure the plugin properly, it’s trivial to access the navigation stack. To push the ingredient detail page from a widget it’s easy like this:

Convert the URL to the stack of items

The Flutter’s RouteInformationParser is the object designated to convert incoming URLs back and forth.

I want to use an URL with a format like this: /section/ingredients/ingredient/1/recipe/2/ingredient/2 (and so on). It’s kind of straightforward to parse it, because this is a series of key-value pairs:

The code is a bit verbose but it’s also very simple:

I start by exploding the URL in its components. I take key-value pairs and I validate the received identifiers: if the value is valid, I insert the proper NavigationStackItem , otherwise I insert notFound . If at the end of the cycle I don’t find an appSection as the first item of the stack (e.g.: the URL was only / ), I prepend it.

The conversion from NavigationStack to URL is really trivial as we leverage the when construct, also from freezed package:

Routing!

Last but not least, we have to actually route users through the pages. Flutter’s RouterDelegate is the glue that sticks the NavigationStack , the data generated by the RouterInformationParser and the Navigator widget displayed to our users.

First of all, we need to have access to the NavigationStack . The delegate instance will observe it and will respond to its changes.

Then we actually convert the stack items to Flutter’s Page instances by implementing the build method: we just need to return a Navigator widget with a fixed navigatorKey and the pages array calculated by mapping stack entries to MaterialPage objects. onPopPage implementation— which is called whenever a user taps on the back button — is easy as popping an item from the observed stack.

If you haven’t recognized mapIndexed, don’t worry: it’s just a small smart custom extension to Iterable!

The last bit of RouterDelegate tells the app what to do when a new route comes in or how to deal with browser history:

The configuration passed to newRoutePath method is the instance created inside the parser: we replace the items inside the managed stack and we are good to go.

The final app

Since we haven’t introduced new concepts, we can use MaterialApp.router convenience method directly:

And that’s it! It even works with browser history if you are using Flutter Web:

You could read and download the full example by visiting its GitHub repo.